Repository: polakowo/vectorbt (language: python)

Issue:
Development



Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index d625167a..24479793 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,25 +3,35 @@ test: &test
   language: python
   install:
     - ./scripts/install-talib.sh
-    - pip install .[full,cov]
+    - pip install .[full]
   script:
     - export NUMBA_BOUNDSCHECK=1
     - pytest tests/
     - export NUMBA_DISABLE_JIT=1
-    - pytest --cov=vectorbt tests/
-  after_success:
-    - if [[ $TRAVIS_PYTHON_VERSION == 3.6 ]]; then codecov; fi
+    - pytest tests/
 
 jobs:
   include:
     - <<: *test
-      python: 3.6
+      python: "3.6"
     - <<: *test
-      python: 3.7
+      python: "3.7"
     - <<: *test
-      python: 3.8
+      python: "3.8"
     - <<: *test
-      python: 3.9
+      python: "3.9"
+    - stage: test-cov
+      language: python
+      python: "3.6"
+      install:
+        - ./scripts/install-talib.sh
+        - pip install .[full,cov]
+      script:
+        - export NUMBA_BOUNDSCHECK=1
+        - export NUMBA_DISABLE_JIT=1
+        - pytest --cov=vectorbt tests/
+      after_success:
+        - codecov
     - stage: deploy-pypi
       language: python
       python: "3.6"
@@ -65,6 +75,8 @@ jobs:
 stages:
   - name: test
     if: (type = pull_request) OR (commit_message =~ /(\[ci test\]|\[ci deploy\])/)
+  - name: test-cov
+    if: (type = pull_request) OR (commit_message =~ /(\[ci test\]|\[ci test-cov\]|\[ci deploy\])/)
   - name: deploy-pypi
     if: (type != pull_request) AND (commit_message =~ /(\[ci deploy-pypi\]|\[ci deploy\])/)
   - name: deploy-pages
diff --git a/README.md b/README.md
index 5c477993..53aa4d2c 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 [![PyPi](https://img.shields.io/pypi/v/vectorbt?color=blueviolet)](https://pypi.org/project/vectorbt)
 [![Build Status](https://travis-ci.com/polakowo/vectorbt.svg?branch=master)](https://travis-ci.com/polakowo/vectorbt)
 [![codecov](https://codecov.io/gh/polakowo/vectorbt/branch/master/graph/badge.svg?token=YTLNAI7PS3)](https://codecov.io/gh/polakowo/vectorbt)
-[![Website](https://img.shields.io/website?url=https%3A%2F%2Fpolakowo.io%2Fvectorbt%2Fdocs%2Findex.html)](https://polakowo.io/vectorbt)
+[![Website](https://img.shields.io/website?url=https://vectorbt.dev/)](https://vectorbt.dev/)
 [![Downloads](https://pepy.tech/badge/vectorbt)](https://pepy.tech/project/vectorbt)
 [![Binder](https://img.shields.io/badge/launch-binder-d6604a)](https://mybinder.org/v2/gh/polakowo/vectorbt/HEAD?urlpath=lab)
 [![Join the chat at https://gitter.im/vectorbt/community](https://badges.gitter.im/vectorbt.svg)](https://gitter.im/vectorbt/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@@ -171,7 +171,7 @@ Max Drawdown Duration                  760 days 00:00:00
 Total Trades                                          54
 Total Closed Trades                                   53
 Total Open Trades                                      1
-Open Trade P&L                              67287.940601
+Open Trade PnL                              67287.940601
 Win Rate [%]                                   52.830189
 Best Trade [%]                               1075.803607
 Worst Trade [%]                               -29.593414
@@ -208,7 +208,7 @@ def plot(index, bbands):
     bbands = bbands.loc[index]
     fig = vbt.make_subplots(
         rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.15,
-        subplot_titles=(*symbols, '%B', 'Bandwidth'))
+        subplot_titles=('%B', 'Bandwidth'))
     fig.update_layout(template='vbt_dark', showlegend=False, width=750, height=400)
     bbands.percent_b.vbt.ts_heatmap(
         trace_kwargs=dict(zmin=0, zmid=0.5, zmax=1, colorscale='Spectral', colorbar=dict(
@@ -437,6 +437,7 @@ sma_timeperiod    2    3
 Start                                        0
 End                                          7
 Period                         8 days 00:00:00
+Coverage [%]                              50.0
 Total Records                                2
 Total Recovered Drawdowns                    1
 Total Active Drawdowns                       1
@@ -447,13 +448,13 @@ Active Recovery Return [%]               100.0
 Active Recovery Duration       1 days 00:00:00
 Max Drawdown [%]                     33.333333
 Avg Drawdown [%]                     33.333333
-Max Drawdown Duration          2 days 00:00:00
-Avg Drawdown Duration          2 days 00:00:00
+Max Drawdown Duration          1 days 00:00:00
+Avg Drawdown Duration          1 days 00:00:00
 Max Recovery Return [%]                   50.0
 Avg Recovery Return [%]                   50.0
 Max Recovery Duration          1 days 00:00:00
 Avg Recovery Duration          1 days 00:00:00
-Avg Recovery Duration Ratio                0.5
+Avg Recovery Duration Ratio                1.0
 dtype: object
 ```
 
@@ -476,7 +477,7 @@ dtype: object
 
 ### Documentation
 
-Head over to the [documentation](https://polakowo.io/vectorbt/docs/index.html) to get started.
+Head over to the [documentation](https://vectorbt.dev/docs/index.html) to get started.
 
 ### Notebooks
 
diff --git a/apps/candlestick-patterns/Dockerfile b/apps/candlestick-patterns/Dockerfile
index df49ec40..eff4e463 100644
--- a/apps/candlestick-patterns/Dockerfile
+++ b/apps/candlestick-patterns/Dockerfile
@@ -4,7 +4,7 @@ RUN apt-get -y update && apt-get -y install gcc curl make
 
 RUN pip install --upgrade pip
 # Required by TA-Lib and numba
-RUN pip install numpy==1.19.4
+RUN pip install numpy>=1.19.4
 
 RUN curl -O https://netcologne.dl.sourceforge.net/project/ta-lib/ta-lib/0.4.0/ta-lib-0.4.0-src.tar.gz \
     && tar -xzf ta-lib-0.4.0-src.tar.gz \
diff --git a/apps/candlestick-patterns/app.py b/apps/candlestick-patterns/app.py
index 672a5fac..8f0c4bb7 100644
--- a/apps/candlestick-patterns/app.py
+++ b/apps/candlestick-patterns/app.py
@@ -19,17 +19,18 @@
 import random
 import yfinance as yf
 import talib
-from collections import OrderedDict
 from talib import abstract
 from talib._ta_lib import (
     CandleSettingType,
     RangeType,
     _ta_set_candle_settings
 )
-import vectorbt as vbt
 from vectorbt import settings
 from vectorbt.utils.config import merge_dicts
 from vectorbt.utils.colors import adjust_opacity
+from vectorbt.utils.template import deep_substitute
+from vectorbt.portfolio.enums import Direction, ConflictMode
+from vectorbt.portfolio.base import Portfolio
 
 USE_CACHING = os.environ.get(
     "USE_CACHING",
@@ -76,10 +77,19 @@
 intervals = ['1m', '2m', '5m', '15m', '30m', '60m', '90m', '1d', '5d', '1wk', '1mo', '3mo']
 patterns = talib.get_function_groups()['Pattern Recognition']
 stats_table_columns = ["Metric", "Buy & Hold", "Random (Median)", "Strategy", "Z-Score"]
-directions = vbt.portfolio.enums.Direction._fields
-conflict_modes = vbt.portfolio.enums.ConflictMode._fields
+directions = Direction._fields
+conflict_modes = ConflictMode._fields
 plot_types = ['OHLC', 'Candlestick']
-subplots = OrderedDict([(k, v['title']) for k, v in vbt.Portfolio.subplot_settings.items()])
+
+# Populate subplots
+all_subplots = {}
+for k, v in Portfolio.subplots.items():
+    trades_sub_v = deep_substitute(v, mapping=dict(use_positions=False), safe=True)
+    all_subplots[k] = trades_sub_v
+    positions_sub_v = deep_substitute(v, mapping=dict(use_positions=True), safe=True)
+    if positions_sub_v['title'] != v['title']:
+        positions_sub_v['use_positions'] = True
+        all_subplots[k.replace('trade', 'position')] = positions_sub_v
 
 # Colors
 color_schema = settings['plotting']['color_schema']
@@ -284,8 +294,10 @@
                                                                 html.Label("Select subplots:"),
                                                                 dcc.Dropdown(
                                                                     id="subplot_dropdown",
-                                                                    options=[{"value": k, "label": v}
-                                                                             for k, v in subplots.items()],
+                                                                    options=[
+                                                                        {"value": k, "label": v['title']}
+                                                                        for k, v in all_subplots.items()
+                                                                    ],
                                                                     multi=True,
                                                                     value=default_subplots,
                                                                 ),
@@ -897,6 +909,9 @@
                                             options=[{
                                                 "label": "Include open trades in stats",
                                                 "value": "incl_open"
+                                            }, {
+                                                "label": "Use positions instead of trades in stats",
+                                                "value": "use_positions"
                                             }],
                                             value=default_stats_options,
                                             style={
@@ -1281,7 +1296,7 @@ def _shuffle_along_axis(a, axis):
 
     # Simulate portfolio
     def _simulate_portfolio(size, init_cash='autoalign'):
-        return vbt.Portfolio.from_signals(
+        return Portfolio.from_signals(
             close=df['Close'],
             entries=size > 0,
             exits=size < 0,
@@ -1358,9 +1373,9 @@ def update_stats(window_width, subplots, df_json, symbol, interval, date_range,
         entry_dates, exit_dates, fees, fixed_fees, slippage, direction, conflict_mode,
         sim_options, n_random_strat, prob_options, entry_prob, exit_prob)
 
-    subplot_kwags = dict()
+    subplot_settings = dict()
     if 'cum_returns' in subplots:
-        subplot_kwags['cum_returns_kwargs'] = dict(
+        subplot_settings['cum_returns'] = dict(
             benchmark_kwargs=dict(
                 trace_kwargs=dict(
                     line=dict(
@@ -1372,8 +1387,8 @@ def update_stats(window_width, subplots, df_json, symbol, interval, date_range,
         )
     height = int(6 / 21 * 2 / 3 * window_width)
     fig = main_portfolio.plot(
-        subplots=subplots,
-        **subplot_kwags,
+        subplots={k: all_subplots[k] for k in subplots},
+        subplot_settings=subplot_settings,
         **merge_dicts(
             default_layout,
             dict(
@@ -1402,9 +1417,10 @@ def _metric_to_str(x):
         return str(x)
 
     incl_open = 'incl_open' in stats_options
-    main_stats = main_portfolio.stats(incl_open=incl_open)
-    hold_stats = hold_portfolio.stats(incl_open=True)
-    rand_stats = rand_portfolio.stats(incl_open=incl_open, agg_func=None)
+    use_positions = 'use_positions' in stats_options
+    main_stats = main_portfolio.stats(settings=dict(incl_open=incl_open, use_positions=use_positions))
+    hold_stats = hold_portfolio.stats(settings=dict(incl_open=True, use_positions=use_positions))
+    rand_stats = rand_portfolio.stats(settings=dict(incl_open=incl_open, use_positions=use_positions), agg_func=None)
     rand_stats_median = rand_stats.iloc[:, 3:].median(axis=0)
     rand_stats_mean = rand_stats.iloc[:, 3:].mean(axis=0)
     rand_stats_std = rand_stats.iloc[:, 3:].std(axis=0, ddof=0)
diff --git a/apps/candlestick-patterns/requirements.txt b/apps/candlestick-patterns/requirements.txt
index a787c288..7d7f5db0 100644
--- a/apps/candlestick-patterns/requirements.txt
+++ b/apps/candlestick-patterns/requirements.txt
@@ -1,11 +1,11 @@
-numpy==1.20.3
-pandas==1.2.4
-dash==1.20.0
-dash_bootstrap_components==0.12.2
-gunicorn==20.1.0
-Flask-Caching==1.10.1
-TA-Lib==0.4.20
-ipywidgets==7.6.3
-matplotlib==3.4.2
-vectorbt==0.18.2
-yfinance>=0.1.59
\ No newline at end of file
+numpy>=1.21.0
+pandas>=1.3.0
+dash>=1.21.0
+dash_bootstrap_components>=0.13.0
+gunicorn>=20.1.0
+Flask-Caching>=1.10.1
+TA-Lib>=0.4.21
+ipywidgets>=7.6.3
+matplotlib>=3.4.2
+vectorbt>=0.20.0
+yfinance>=0.1.62
\ No newline at end of file
diff --git a/docs/img/ohlcstex.svg b/docs/img/OHLCSTX.svg
similarity index 100%
rename from docs/img/ohlcstex.svg
rename to docs/img/OHLCSTX.svg
diff --git a/docs/img/drawdowns_drawdown_histplot.svg b/docs/img/drawdowns_drawdown_histplot.svg
deleted file mode 100644
index b8cb06a0..00000000
--- a/docs/img/drawdowns_drawdown_histplot.svg
+++ /dev/null
@@ -1,1 +0,0 @@
-<svg class="main-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="700" height="350" style="" viewBox="0 0 700 350"><rect x="0" y="0" width="700" height="350" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/><defs id="defs-5c5e94"><g class="clips"><clipPath id="clip5c5e94xyplot" class="plotclip"><rect width="637" height="274"/></clipPath><clipPath class="axesclip" id="clip5c5e94x"><rect x="33" y="0" width="637" height="350"/></clipPath><clipPath class="axesclip" id="clip5c5e94y"><rect x="0" y="46" width="700" height="274"/></clipPath><clipPath class="axesclip" id="clip5c5e94xy"><rect x="33" y="46" width="637" height="274"/></clipPath></g><g class="gradients"/></defs><g class="bglayer"><rect class="bg" x="33" y="46" width="637" height="274" style="fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;"/></g><g class="layer-below"><g class="imagelayer"/><g class="shapelayer"/></g><g class="cartesianlayer"><g class="subplot xy"><g class="layer-subplot"><g class="shapelayer"/><g class="imagelayer"/></g><g class="gridlayer"><g class="x"/><g class="y"><path class="ygrid crisp" transform="translate(0,276.62)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,233.23)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,189.85)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,146.47)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,103.08)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,59.7)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g></g><g class="zerolinelayer"><path class="yzl zl crisp" transform="translate(0,320)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 2px;"/></g><path class="xlines-below"/><path class="ylines-below"/><g class="overlines-below"/><g class="xaxislayer-below"/><g class="yaxislayer-below"/><g class="overaxes-below"/><g class="plot" transform="translate(33, 46)" clip-path="url('#clip5c5e94xyplot')"><g class="barlayer mlayer"><g class="trace bars" shape-rendering="crispEdges" style="opacity: 1;"><g class="points"><g class="point"><path d="M0,274V187.23H13V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M13,274V274H26V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M26,274V274H39V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M39,274V274H52V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M52,274V274H65V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M65,274V274H78V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M78,274V274H91V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M91,274V274H104V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M104,274V274H117V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M117,274V274H130V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M130,274V274H143V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M143,274V274H156V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M156,274V274H169V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M169,274V274H182V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M182,274V274H195V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M195,274V274H208V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M208,274V274H221V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M221,274V274H234V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M234,274V274H247V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M247,274V274H260V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M260,274V274H273V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M273,274V274H286V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M286,274V274H299V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M299,274V274H312V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M312,274V274H325V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M325,274V274H338V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M338,274V274H351V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M351,274V274H364V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M364,274V274H377V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M377,274V274H390V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M390,274V274H403V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M403,274V274H416V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M416,274V187.23H429V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M429,274V274H442V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M442,274V274H455V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M455,274V274H468V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M468,274V187.23H481V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M481,274V187.23H494V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M494,274V274H507V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M507,274V187.23H520V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M520,274V274H533V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M533,274V274H546V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M546,274V187.23H559V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M559,274V274H572V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M572,274V187.23H585V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M585,274V187.23H598V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M598,274V100.47H611V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M611,274V13.7H624V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="point"><path d="M624,274V187.23H637V274Z" style="vector-effect: non-scaling-stroke; opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g></g></g></g></g><g class="overplot"/><path class="xlines-above crisp" d="M0,0" style="fill: none;"/><path class="ylines-above crisp" d="M0,0" style="fill: none;"/><g class="overlines-above"/><g class="xaxislayer-above"><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(150,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">−0.4</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(280,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">−0.3</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(410,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">−0.2</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(540,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">−0.1</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(670,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">0</text></g></g><g class="yaxislayer-above"><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,320)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">0</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,276.62)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">0.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,233.23)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,189.85)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,146.47)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,103.08)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,59.7)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">3</text></g></g><g class="overaxes-above"/></g></g><g class="polarlayer"/><g class="ternarylayer"/><g class="geolayer"/><g class="funnelarealayer"/><g class="pielayer"/><g class="treemaplayer"/><g class="sunburstlayer"/><g class="glimages"/><defs id="topdefs-5c5e94"><g class="clips"/><clipPath id="legend5c5e94"><rect width="78" height="29" x="0" y="0"/></clipPath></defs><g class="layer-above"><g class="imagelayer"/><g class="shapelayer"/></g><g class="infolayer"><g class="legend" pointer-events="all" transform="translate(592, 11.519999999999996)"><rect class="bg" shape-rendering="crispEdges" style="stroke: rgb(68, 68, 68); stroke-opacity: 1; fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 0px;" width="78" height="29" x="0" y="0"/><g class="scrollbox" transform="translate(0, 0)" clip-path="url('#legend5c5e94')"><g class="groups"><g class="traces" transform="translate(0, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Close</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="legendundefined" d="M6,6H-6V-6H6Z" transform="translate(20,0)" style="stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="74.859375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g></g></g><rect class="scrollbar" rx="20" ry="3" width="0" height="0" style="fill: rgb(128, 139, 164); fill-opacity: 1;" x="0" y="0"/></g><g class="g-gtitle"/><g class="g-xtitle"/><g class="g-ytitle"/></g></svg>
\ No newline at end of file
diff --git a/docs/img/drawdowns_plot.svg b/docs/img/drawdowns_plot.svg
index 47d8977c..265e5598 100644
--- a/docs/img/drawdowns_plot.svg
+++ b/docs/img/drawdowns_plot.svg
@@ -1,1 +1,1 @@
-<svg class="main-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="700" height="350" style="" viewBox="0 0 700 350"><rect x="0" y="0" width="700" height="350" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/><defs id="defs-9ac71a"><g class="clips"><clipPath id="clip9ac71axyplot" class="plotclip"><rect width="637" height="274"/></clipPath><clipPath class="axesclip" id="clip9ac71ax"><rect x="33" y="0" width="637" height="350"/></clipPath><clipPath class="axesclip" id="clip9ac71ay"><rect x="0" y="46" width="700" height="274"/></clipPath><clipPath class="axesclip" id="clip9ac71axy"><rect x="33" y="46" width="637" height="274"/></clipPath></g><g class="gradients"/></defs><g class="bglayer"><rect class="bg" x="33" y="46" width="637" height="274" style="fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;"/></g><g class="layer-below"><g class="imagelayer"/><g class="shapelayer"><path data-index="0" fill-rule="evenodd" d="M149.34,320H230.08V46H149.34Z" clip-path="url('#clip9ac71ax')" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 0, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="1" fill-rule="evenodd" d="M230.08,320H310.82V46H230.08Z" clip-path="url('#clip9ac71ax')" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(0, 128, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="2" fill-rule="evenodd" d="M391.56,320H633.78V46H391.56Z" clip-path="url('#clip9ac71ax')" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 165, 0); fill-opacity: 1; stroke-width: 0px;"/></g></g><g class="cartesianlayer"><g class="subplot xy"><g class="layer-subplot"><g class="shapelayer"/><g class="imagelayer"/></g><g class="gridlayer"><g class="x"><path class="xgrid crisp" transform="translate(149.34,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(230.08,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(310.82,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(391.56,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(472.3,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(553.04,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(633.78,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g><g class="y"><path class="ygrid crisp" transform="translate(0,301.93)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,242.46)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,183)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,123.54)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,64.07)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g></g><g class="zerolinelayer"><path class="xzl zl crisp" transform="translate(68.6,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 2px;"/></g><path class="xlines-below"/><path class="ylines-below"/><g class="overlines-below"/><g class="xaxislayer-below"/><g class="yaxislayer-below"/><g class="overaxes-below"/><g class="plot" transform="translate(33, 46)" clip-path="url('#clip9ac71axyplot')"><g class="scatterlayer mlayer"><g class="trace scatter trace42755841-5e92-4c65-9bcc-b903fddd1701" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"><path class="js-line" d="M35.6,255.93L116.34,137L197.08,255.93L358.56,18.07L439.3,137L520.04,255.93L600.78,137" style="vector-effect: non-scaling-stroke; fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px; opacity: 1;"/></g><g class="points"><path class="point" transform="translate(35.6,255.93)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(116.34,137)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(197.08,255.93)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(277.82,137)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(358.56,18.07)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(439.3,137)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(520.04,255.93)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(600.78,137)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter traceeecb2b94-fb8a-49d3-86c7-ec530face83d" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(116.34,137)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(358.56,18.07)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter traceaa7991f6-0985-44b2-8d1d-34f94f61783c" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(197.08,255.93)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace3f11e5fe-022e-4eac-8971-a66bdc0f6ced" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(277.82,137)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace194db103-66a5-42c2-93e1-82908823b53c" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(600.78,137)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g><g class="text"/></g></g></g><g class="overplot"/><path class="xlines-above crisp" d="M0,0" style="fill: none;"/><path class="ylines-above crisp" d="M0,0" style="fill: none;"/><g class="overlines-above"/><g class="xaxislayer-above"><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(68.6,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">0</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(149.34,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(230.08,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(310.82,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">3</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(391.56,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">4</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(472.3,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">5</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(553.04,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">6</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(633.78,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">7</text></g></g><g class="yaxislayer-above"><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,301.93)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,242.46)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,183)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,123.54)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,64.07)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">3</text></g></g><g class="overaxes-above"/></g></g><g class="polarlayer"/><g class="ternarylayer"/><g class="geolayer"/><g class="funnelarealayer"/><g class="pielayer"/><g class="treemaplayer"/><g class="sunburstlayer"/><g class="glimages"/><defs id="topdefs-9ac71a"><g class="clips"/><clipPath id="legend9ac71a"><rect width="372" height="29" x="0" y="0"/></clipPath></defs><g class="layer-above"><g class="imagelayer"/><g class="shapelayer"/></g><g class="infolayer"><g class="legend" pointer-events="all" transform="translate(298, 11.519999999999996)"><rect class="bg" shape-rendering="crispEdges" style="stroke: rgb(68, 68, 68); stroke-opacity: 1; fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 0px;" width="372" height="29" x="0" y="0"/><g class="scrollbox" transform="translate(0, 0)" clip-path="url('#legend9ac71a')"><g class="groups"><g class="traces" transform="translate(0, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Peak</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="71.203125" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(73.703125, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Valley</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="78.75" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(154.953125, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Recovery/Peak</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="132.15625" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(289.609375, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Active</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="79.234375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g></g></g><rect class="scrollbar" rx="20" ry="3" width="0" height="0" style="fill: rgb(128, 139, 164); fill-opacity: 1;" x="0" y="0"/></g><g class="g-gtitle"/><g class="g-xtitle"/><g class="g-ytitle"/></g></svg>
\ No newline at end of file
+<svg class="main-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="700" height="350" style="" viewBox="0 0 700 350"><rect x="0" y="0" width="700" height="350" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/><defs id="defs-64313d"><g class="clips"><clipPath id="clip64313dxyplot" class="plotclip"><rect width="637" height="261"/></clipPath><clipPath class="axesclip" id="clip64313dx"><rect x="33" y="0" width="637" height="350"/></clipPath><clipPath class="axesclip" id="clip64313dy"><rect x="0" y="46" width="700" height="261"/></clipPath><clipPath class="axesclip" id="clip64313dxy"><rect x="33" y="46" width="637" height="261"/></clipPath></g><g class="gradients"/><g class="patterns"/></defs><g class="bglayer"><rect class="bg" x="33" y="46" width="637" height="261" style="fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;"/></g><g class="layer-below"><g class="imagelayer"/><g class="shapelayer"><path data-index="0" fill-rule="evenodd" d="M149.34,307H230.08V46H149.34Z" clip-path="url(#clip64313dx)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 0, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="1" fill-rule="evenodd" d="M230.08,307H310.82V46H230.08Z" clip-path="url(#clip64313dx)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(0, 128, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="2" fill-rule="evenodd" d="M391.56,307H633.77V46H391.56Z" clip-path="url(#clip64313dx)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 165, 0); fill-opacity: 1; stroke-width: 0px;"/></g></g><g class="cartesianlayer"><g class="subplot xy"><g class="layer-subplot"><g class="shapelayer"/><g class="imagelayer"/></g><g class="gridlayer"><g class="x"><path class="xgrid crisp" transform="translate(68.6,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(149.34,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(230.08,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(310.82,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(391.56,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(472.3,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(553.04,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(633.77,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g><g class="y"><path class="ygrid crisp" transform="translate(0,289.58000000000004)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,233.04)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,176.5)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,119.96)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,63.43)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g></g><g class="zerolinelayer"/><path class="xlines-below"/><path class="ylines-below"/><g class="overlines-below"/><g class="xaxislayer-below"/><g class="yaxislayer-below"/><g class="overaxes-below"/><g class="plot" transform="translate(33,46)" clip-path="url(#clip64313dxyplot)"><g class="scatterlayer mlayer"><g class="trace scatter trace006a992f-1cd8-4fdd-b048-55dab7f561ca" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"><path class="js-line" d="M35.6,243.58L116.34,130.5L197.08,243.58L358.56,17.43L439.3,130.5L520.04,243.58L600.77,130.5" style="vector-effect: non-scaling-stroke; fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px; opacity: 1;"/></g><g class="points"><path class="point" transform="translate(35.6,243.58)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(116.34,130.5)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(197.08,243.58)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(277.82,130.5)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(358.56,17.43)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(439.3,130.5)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(520.04,243.58)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(600.77,130.5)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter traceb9cd4de7-08fe-4d71-8481-d45b61400b01" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(116.34,130.5)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(358.56,17.43)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace44d54095-1f93-4f31-bab8-14d85cb244b7" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(197.08,243.58)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter tracef15df42a-bbbc-4c76-8990-1d76db5b55a0" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(277.82,130.5)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace08124ec6-6a07-4dc1-9648-900d8261f21c" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(600.77,130.5)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g><g class="text"/></g></g></g><g class="overplot"/><path class="xlines-above crisp" d="M0,0" style="fill: none;"/><path class="ylines-above crisp" d="M0,0" style="fill: none;"/><g class="overlines-above"/><g class="xaxislayer-above"><g class="xtick"><text text-anchor="middle" x="0" y="320" transform="translate(68.6,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;"><tspan class="line" dy="0em" x="0" y="320">Jan 1</tspan><tspan class="line" dy="1.3em" x="0" y="320">2020</tspan></text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(149.34,0)">Jan 2</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(230.08,0)">Jan 3</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(310.82,0)">Jan 4</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(391.56,0)">Jan 5</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(472.3,0)">Jan 6</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(553.04,0)">Jan 7</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(633.77,0)">Jan 8</text></g></g><g class="yaxislayer-above"><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,289.58000000000004)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;">1</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,233.04)">1.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,176.5)">2</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,119.96)">2.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,63.43)">3</text></g></g><g class="overaxes-above"/></g></g><g class="polarlayer"/><g class="ternarylayer"/><g class="geolayer"/><g class="funnelarealayer"/><g class="pielayer"/><g class="iciclelayer"/><g class="treemaplayer"/><g class="sunburstlayer"/><g class="glimages"/><defs id="topdefs-64313d"><g class="clips"/><clipPath id="legend64313d"><rect width="446" height="29" x="0" y="0"/></clipPath></defs><g class="layer-above"><g class="imagelayer"/><g class="shapelayer"/></g><g class="infolayer"><g class="legend" pointer-events="all" transform="translate(224,11.779999999999994)"><rect class="bg" shape-rendering="crispEdges" style="stroke: rgb(68, 68, 68); stroke-opacity: 1; fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 0px;" width="446" height="29" x="0" y="0"/><g class="scrollbox" transform="" clip-path="url(#legend64313d)"><g class="groups"><g class="traces" transform="translate(0,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Price</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"><path class="js-line" d="M5,0h30" style="fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px;"/></g><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="71.5625" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(74.0625,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Peak</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="71.203125" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(147.765625,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Valley</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="78.75" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(229.015625,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Recovery/Peak</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="132.15625" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(363.671875,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Active</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="79.234375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g></g></g><rect class="scrollbar" rx="20" ry="3" width="0" height="0" style="fill: rgb(128, 139, 164); fill-opacity: 1;" x="0" y="0"/></g><g class="g-gtitle"/><g class="g-xtitle"/><g class="g-ytitle"/></g></svg>
\ No newline at end of file
diff --git a/docs/img/orders_plot.svg b/docs/img/orders_plot.svg
index 73fb5fe8..0c56224a 100644
--- a/docs/img/orders_plot.svg
+++ b/docs/img/orders_plot.svg
@@ -1,1 +1,1 @@
-<svg class="main-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="700" height="350" style="" viewBox="0 0 700 350"><rect x="0" y="0" width="700" height="350" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/><defs id="defs-d47ccf"><g class="clips"><clipPath id="clipd47ccfxyplot" class="plotclip"><rect width="637" height="274"/></clipPath><clipPath class="axesclip" id="clipd47ccfx"><rect x="33" y="0" width="637" height="350"/></clipPath><clipPath class="axesclip" id="clipd47ccfy"><rect x="0" y="46" width="700" height="274"/></clipPath><clipPath class="axesclip" id="clipd47ccfxy"><rect x="33" y="46" width="637" height="274"/></clipPath></g><g class="gradients"/></defs><g class="bglayer"><rect class="bg" x="33" y="46" width="637" height="274" style="fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;"/></g><g class="layer-below"><g class="imagelayer"/><g class="shapelayer"/></g><g class="cartesianlayer"><g class="subplot xy"><g class="layer-subplot"><g class="shapelayer"/><g class="imagelayer"/></g><g class="gridlayer"><g class="x"><path class="xgrid crisp" transform="translate(210.67,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(351.5,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(492.32,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(633.15,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g><g class="y"><path class="ygrid crisp" transform="translate(0,301.3)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,242.15)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,183)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,123.85)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,64.7)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g></g><g class="zerolinelayer"><path class="xzl zl crisp" transform="translate(69.85,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 2px;"/></g><path class="xlines-below"/><path class="ylines-below"/><g class="overlines-below"/><g class="xaxislayer-below"/><g class="yaxislayer-below"/><g class="overaxes-below"/><g class="plot" transform="translate(33, 46)" clip-path="url('#clipd47ccfxyplot')"><g class="scatterlayer mlayer"><g class="trace scatter tracecda5071e-d0be-4c2c-9c0e-a25806a68573" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"><path class="js-line" d="M36.85,255.3L318.5,18.7L459.32,137L600.15,255.3" style="vector-effect: non-scaling-stroke; fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px; opacity: 1;"/></g><g class="points"><path class="point" transform="translate(36.85,255.3)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(177.67,137)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(318.5,18.7)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(459.32,137)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(600.15,255.3)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter tracecd6cae66-fe64-465c-8382-4ad5d8cd5811" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(36.85,255.3)" d="M-4.62,2H4.62L0,-4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(177.67,137)" d="M-4.62,2H4.62L0,-4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(318.5,18.7)" d="M-4.62,2H4.62L0,-4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(459.32,137)" d="M-4.62,2H4.62L0,-4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace178df77e-3393-4154-a267-5459f2ceaf15" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(600.15,255.3)" d="M-4.62,-2H4.62L0,4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g><g class="text"/></g></g></g><g class="overplot"/><path class="xlines-above crisp" d="M0,0" style="fill: none;"/><path class="ylines-above crisp" d="M0,0" style="fill: none;"/><g class="overlines-above"/><g class="xaxislayer-above"><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(69.85,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">0</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(210.67,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(351.5,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(492.32,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">3</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(633.15,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">4</text></g></g><g class="yaxislayer-above"><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,301.3)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,242.15)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,183)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,123.85)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,64.7)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">3</text></g></g><g class="overaxes-above"/></g></g><g class="polarlayer"/><g class="ternarylayer"/><g class="geolayer"/><g class="funnelarealayer"/><g class="pielayer"/><g class="treemaplayer"/><g class="sunburstlayer"/><g class="glimages"/><defs id="topdefs-d47ccf"><g class="clips"/><clipPath id="legendd47ccf"><rect width="213" height="29" x="0" y="0"/></clipPath></defs><g class="layer-above"><g class="imagelayer"/><g class="shapelayer"/></g><g class="infolayer"><g class="legend" pointer-events="all" transform="translate(457, 11.519999999999996)"><rect class="bg" shape-rendering="crispEdges" style="stroke: rgb(68, 68, 68); stroke-opacity: 1; fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 0px;" width="213" height="29" x="0" y="0"/><g class="scrollbox" transform="translate(0, 0)" clip-path="url('#legendd47ccf')"><g class="groups"><g class="traces" transform="translate(0, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Close</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"><path class="js-line" d="M5,0h30" style="fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px;"/></g><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="74.859375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(77.359375, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Buy</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M-4.62,2H4.62L0,-4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="65.421875" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(145.28125, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Sell</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M-4.62,-2H4.62L0,4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="64.4375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g></g></g><rect class="scrollbar" rx="20" ry="3" width="0" height="0" style="fill: rgb(128, 139, 164); fill-opacity: 1;" x="0" y="0"/></g><g class="g-gtitle"/><g class="g-xtitle"/><g class="g-ytitle"/></g></svg>
\ No newline at end of file
+<svg class="main-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="700" height="350" style="" viewBox="0 0 700 350"><rect x="0" y="0" width="700" height="350" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/><defs id="defs-7b8cfc"><g class="clips"><clipPath id="clip7b8cfcxyplot" class="plotclip"><rect width="637" height="261"/></clipPath><clipPath class="axesclip" id="clip7b8cfcx"><rect x="33" y="0" width="637" height="350"/></clipPath><clipPath class="axesclip" id="clip7b8cfcy"><rect x="0" y="46" width="700" height="261"/></clipPath><clipPath class="axesclip" id="clip7b8cfcxy"><rect x="33" y="46" width="637" height="261"/></clipPath></g><g class="gradients"/><g class="patterns"/></defs><g class="bglayer"><rect class="bg" x="33" y="46" width="637" height="261" style="fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;"/></g><g class="layer-below"><g class="imagelayer"/><g class="shapelayer"/></g><g class="cartesianlayer"><g class="subplot xy"><g class="layer-subplot"><g class="shapelayer"/><g class="imagelayer"/></g><g class="gridlayer"><g class="x"><path class="xgrid crisp" transform="translate(69.85,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(210.67,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(351.5,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(492.33,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(633.15,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g><g class="y"><path class="ygrid crisp" transform="translate(0,288.95)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,232.73)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,176.5)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,120.27)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,64.05)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g></g><g class="zerolinelayer"/><path class="xlines-below"/><path class="ylines-below"/><g class="overlines-below"/><g class="xaxislayer-below"/><g class="yaxislayer-below"/><g class="overaxes-below"/><g class="plot" transform="translate(33,46)" clip-path="url(#clip7b8cfcxyplot)"><g class="scatterlayer mlayer"><g class="trace scatter tracef4fda11e-5b28-4978-b20a-38c206618842" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"><path class="js-line" d="M36.85,242.95L318.5,18.05L459.33,130.5L600.15,242.95" style="vector-effect: non-scaling-stroke; fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px; opacity: 1;"/></g><g class="points"><path class="point" transform="translate(36.85,242.95)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(177.67,130.5)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(318.5,18.05)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(459.33,130.5)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(600.15,242.95)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter traceedea1786-af01-4850-9bb9-9972ae5f73a8" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(36.85,242.95)" d="M-4.62,2H4.62L0,-4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(177.67,130.5)" d="M-4.62,2H4.62L0,-4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(318.5,18.05)" d="M-4.62,2H4.62L0,-4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(459.33,130.5)" d="M-4.62,2H4.62L0,-4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace1a0441f9-552c-46b6-826e-a99b540582ff" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(600.15,242.95)" d="M-4.62,-2H4.62L0,4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g><g class="text"/></g></g></g><g class="overplot"/><path class="xlines-above crisp" d="M0,0" style="fill: none;"/><path class="ylines-above crisp" d="M0,0" style="fill: none;"/><g class="overlines-above"/><g class="xaxislayer-above"><g class="xtick"><text text-anchor="middle" x="0" y="320" transform="translate(69.85,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;"><tspan class="line" dy="0em" x="0" y="320">Jan 1</tspan><tspan class="line" dy="1.3em" x="0" y="320">2020</tspan></text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(210.67,0)">Jan 2</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(351.5,0)">Jan 3</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(492.33,0)">Jan 4</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(633.15,0)">Jan 5</text></g></g><g class="yaxislayer-above"><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,288.95)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;">1</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,232.73)">1.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,176.5)">2</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,120.27)">2.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,64.05)">3</text></g></g><g class="overaxes-above"/></g></g><g class="polarlayer"/><g class="ternarylayer"/><g class="geolayer"/><g class="funnelarealayer"/><g class="pielayer"/><g class="iciclelayer"/><g class="treemaplayer"/><g class="sunburstlayer"/><g class="glimages"/><defs id="topdefs-7b8cfc"><g class="clips"/><clipPath id="legend7b8cfc"><rect width="209" height="29" x="0" y="0"/></clipPath></defs><g class="layer-above"><g class="imagelayer"/><g class="shapelayer"/></g><g class="infolayer"><g class="legend" pointer-events="all" transform="translate(461,11.779999999999994)"><rect class="bg" shape-rendering="crispEdges" style="stroke: rgb(68, 68, 68); stroke-opacity: 1; fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 0px;" width="209" height="29" x="0" y="0"/><g class="scrollbox" transform="" clip-path="url(#legend7b8cfc)"><g class="groups"><g class="traces" transform="translate(0,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Price</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"><path class="js-line" d="M5,0h30" style="fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px;"/></g><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="71.5625" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(74.0625,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Buy</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M-4.62,2H4.62L0,-4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="65.421875" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(141.984375,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Sell</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M-4.62,-2H4.62L0,4Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="64.4375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g></g></g><rect class="scrollbar" rx="20" ry="3" width="0" height="0" style="fill: rgb(128, 139, 164); fill-opacity: 1;" x="0" y="0"/></g><g class="g-gtitle"/><g class="g-xtitle"/><g class="g-ytitle"/></g></svg>
\ No newline at end of file
diff --git a/docs/img/ranges_plot.svg b/docs/img/ranges_plot.svg
new file mode 100644
index 00000000..b456a17c
--- /dev/null
+++ b/docs/img/ranges_plot.svg
@@ -0,0 +1,1 @@
+<svg class="main-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="700" height="350" style="" viewBox="0 0 700 350"><rect x="0" y="0" width="700" height="350" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/><defs id="defs-df52b6"><g class="clips"><clipPath id="clipdf52b6xyplot" class="plotclip"><rect width="629" height="261"/></clipPath><clipPath class="axesclip" id="clipdf52b6x"><rect x="41" y="0" width="629" height="350"/></clipPath><clipPath class="axesclip" id="clipdf52b6y"><rect x="0" y="46" width="700" height="261"/></clipPath><clipPath class="axesclip" id="clipdf52b6xy"><rect x="41" y="46" width="629" height="261"/></clipPath></g><g class="gradients"/><g class="patterns"/></defs><g class="bglayer"><rect class="bg" x="41" y="46" width="629" height="261" style="fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;"/></g><g class="layer-below"><g class="imagelayer"/><g class="shapelayer"><path data-index="0" fill-rule="evenodd" d="M155.91,307H235.62V46H155.91Z" clip-path="url(#clipdf52b6x)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(0, 128, 128); fill-opacity: 1; stroke-width: 0px;"/><path data-index="1" fill-rule="evenodd" d="M315.33,307H554.46V46H315.33Z" clip-path="url(#clipdf52b6x)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(0, 128, 128); fill-opacity: 1; stroke-width: 0px;"/><path data-index="2" fill-rule="evenodd" d="M634.17,307H634.17V46H634.17Z" clip-path="url(#clipdf52b6x)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 165, 0); fill-opacity: 1; stroke-width: 0px;"/></g></g><g class="cartesianlayer"><g class="subplot xy"><g class="layer-subplot"><g class="shapelayer"/><g class="imagelayer"/></g><g class="gridlayer"><g class="x"><path class="xgrid crisp" transform="translate(76.2,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(155.91,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(235.62,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(315.33,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(395.04,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(474.75,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(554.46,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(634.17,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g><g class="y"><path class="ygrid crisp" transform="translate(0,289.58000000000004)" d="M41,0h629" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,63.42)" d="M41,0h629" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g></g><g class="zerolinelayer"/><path class="xlines-below"/><path class="ylines-below"/><g class="overlines-below"/><g class="xaxislayer-below"/><g class="yaxislayer-below"/><g class="overaxes-below"/><g class="plot" transform="translate(41,46)" clip-path="url(#clipdf52b6xyplot)"><g class="scatterlayer mlayer"><g class="trace scatter traced95f469f-4b36-4689-b80d-d751bcbcb2c7" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"><path class="js-line" d="M35.2,243.58L114.91,17.42L194.62,243.58L274.33,17.42L354.04,17.42L433.75,17.42L513.46,243.58L593.17,17.42" style="vector-effect: non-scaling-stroke; fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px; opacity: 1;"/></g><g class="points"><path class="point" transform="translate(35.2,243.58)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(114.91,17.42)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(194.62,243.58)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(274.33,17.42)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(354.04,17.42)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(433.75,17.42)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(513.46,243.58)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(593.17,17.42)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace78454535-36e2-4550-b58b-00175ba5da43" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(114.91,17.42)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(274.33,17.42)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(593.17,17.42)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace20d090a5-ef83-4cee-99d9-aea6769557e1" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(194.62,243.58)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(513.46,243.58)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace53dbb6e7-19af-4150-a284-7142877b6f03" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(593.17,17.42)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g><g class="text"/></g></g></g><g class="overplot"/><path class="xlines-above crisp" d="M0,0" style="fill: none;"/><path class="ylines-above crisp" d="M0,0" style="fill: none;"/><g class="overlines-above"/><g class="xaxislayer-above"><g class="xtick"><text text-anchor="middle" x="0" y="320" transform="translate(76.2,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;"><tspan class="line" dy="0em" x="0" y="320">Jan 1</tspan><tspan class="line" dy="1.3em" x="0" y="320">2020</tspan></text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(155.91,0)">Jan 2</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(235.62,0)">Jan 3</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(315.33,0)">Jan 4</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(395.04,0)">Jan 5</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(474.75,0)">Jan 6</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(554.46,0)">Jan 7</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(634.17,0)">Jan 8</text></g></g><g class="yaxislayer-above"><g class="ytick"><text text-anchor="end" x="40" y="4.199999999999999" transform="translate(0,289.58000000000004)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;">false</text></g><g class="ytick"><text text-anchor="end" x="40" y="4.199999999999999" transform="translate(0,63.42)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;">true</text></g></g><g class="overaxes-above"/></g></g><g class="polarlayer"/><g class="ternarylayer"/><g class="geolayer"/><g class="funnelarealayer"/><g class="pielayer"/><g class="iciclelayer"/><g class="treemaplayer"/><g class="sunburstlayer"/><g class="glimages"/><defs id="topdefs-df52b6"><g class="clips"/><clipPath id="legenddf52b6"><rect width="311" height="29" x="0" y="0"/></clipPath></defs><g class="layer-above"><g class="imagelayer"/><g class="shapelayer"/></g><g class="infolayer"><g class="legend" pointer-events="all" transform="translate(359,11.779999999999994)"><rect class="bg" shape-rendering="crispEdges" style="stroke: rgb(68, 68, 68); stroke-opacity: 1; fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 0px;" width="311" height="29" x="0" y="0"/><g class="scrollbox" transform="" clip-path="url(#legenddf52b6)"><g class="groups"><g class="traces" transform="translate(0,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Price</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"><path class="js-line" d="M5,0h30" style="fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px;"/></g><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="71.5625" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(74.0625,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Start</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="72.5" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(149.0625,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Closed</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="82.34375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(233.90625,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Open</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.55,0L0,4.55L-4.55,0L0,-4.55Z" style="opacity: 1; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="74.171875" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g></g></g><rect class="scrollbar" rx="20" ry="3" width="0" height="0" style="fill: rgb(128, 139, 164); fill-opacity: 1;" x="0" y="0"/></g><g class="g-gtitle"/><g class="g-xtitle"/><g class="g-ytitle"/></g></svg>
\ No newline at end of file
diff --git a/docs/img/trades_plot.svg b/docs/img/trades_plot.svg
index f0198ee3..16f21b8b 100644
--- a/docs/img/trades_plot.svg
+++ b/docs/img/trades_plot.svg
@@ -1,1 +1,1 @@
-<svg class="main-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="700" height="350" style="" viewBox="0 0 700 350"><rect x="0" y="0" width="700" height="350" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/><defs id="defs-ab296a"><g class="clips"><clipPath id="clipab296axyplot" class="plotclip"><rect width="637" height="274"/></clipPath><clipPath class="axesclip" id="clipab296ax"><rect x="33" y="0" width="637" height="350"/></clipPath><clipPath class="axesclip" id="clipab296ay"><rect x="0" y="46" width="700" height="274"/></clipPath><clipPath class="axesclip" id="clipab296axy"><rect x="33" y="46" width="637" height="274"/></clipPath></g><g class="gradients"/></defs><g class="bglayer"><rect class="bg" x="33" y="46" width="637" height="274" style="fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;"/></g><g class="layer-below"><g class="imagelayer"/><g class="shapelayer"/></g><g class="cartesianlayer"><g class="subplot xy"><g class="layer-subplot"><g class="shapelayer"><path data-index="0" fill-rule="evenodd" d="M69.22999999999999,301.93H163.32V222.64H69.22999999999999Z" clip-path="url('#clipab296axy')" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(0, 128, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="1" fill-rule="evenodd" d="M69.22999999999999,301.93H257.40999999999997V143.36H69.22999999999999Z" clip-path="url('#clipab296axy')" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(0, 128, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="2" fill-rule="evenodd" d="M351.5,64.08H445.59V143.36H351.5Z" clip-path="url('#clipab296axy')" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 0, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="3" fill-rule="evenodd" d="M351.5,64.08H539.6800000000001V222.64H351.5Z" clip-path="url('#clipab296axy')" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 0, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="4" fill-rule="evenodd" d="M351.5,64.08H633.78V301.93H351.5Z" clip-path="url('#clipab296axy')" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 0, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="5" fill-rule="evenodd" d="M351.5,64.08H633.78V301.93H351.5Z" clip-path="url('#clipab296axy')" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 0, 0); fill-opacity: 1; stroke-width: 0px;"/></g><g class="imagelayer"/></g><g class="gridlayer"><g class="x"><path class="xgrid crisp" transform="translate(163.32,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(257.40999999999997,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(351.5,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(445.59,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(539.6800000000001,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(633.78,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g><g class="y"><path class="ygrid crisp" transform="translate(0,301.93)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,262.28)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,222.64)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,183)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,143.36)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,103.72)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,64.08)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g></g><g class="zerolinelayer"><path class="xzl zl crisp" transform="translate(69.22999999999999,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 2px;"/></g><path class="xlines-below"/><path class="ylines-below"/><g class="overlines-below"/><g class="xaxislayer-below"/><g class="yaxislayer-below"/><g class="overaxes-below"/><g class="plot" transform="translate(33, 46)" clip-path="url('#clipab296axyplot')"><g class="scatterlayer mlayer"><g class="trace scatter tracee35b1d18-9166-4b9b-afed-48cc42379027" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"><path class="js-line" d="M36.23,255.93L318.5,18.08L412.59,97.36L600.78,255.93" style="vector-effect: non-scaling-stroke; fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px; opacity: 1;"/></g><g class="points"><path class="point" transform="translate(36.23,255.93)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(130.32,176.64)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(224.41,97.36)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(318.5,18.08)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(412.59,97.36)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(506.68,176.64)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(600.78,255.93)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace3cb507ab-fd44-48e3-9bd2-51e15ef6e89b" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(36.23,255.93)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(36.23,255.93)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(318.5,18.08)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(318.5,18.08)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(318.5,18.08)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(318.5,18.08)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter tracefd5e0192-b5b5-4ce2-ad09-4018f21fde31" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(130.32,176.64)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(224.41,97.36)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter tracec73591df-a5fa-4e80-bd19-b35723e7768a" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(412.59,97.36)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(506.68,176.64)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(600.78,255.93)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace556751d7-145a-47f5-9a0d-8c4ef88880c7" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(600.78,255.93)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g><g class="text"/></g></g></g><g class="overplot"/><path class="xlines-above crisp" d="M0,0" style="fill: none;"/><path class="ylines-above crisp" d="M0,0" style="fill: none;"/><g class="overlines-above"/><g class="xaxislayer-above"><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(69.22999999999999,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">0</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(163.32,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(257.40999999999997,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(351.5,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">3</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(445.59,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">4</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(539.6800000000001,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">5</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(633.78,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">6</text></g></g><g class="yaxislayer-above"><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,301.93)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,262.28)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,222.64)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,183)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,143.36)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">3</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,103.72)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">3.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,64.08)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">4</text></g></g><g class="overaxes-above"/></g></g><g class="polarlayer"/><g class="ternarylayer"/><g class="geolayer"/><g class="funnelarealayer"/><g class="pielayer"/><g class="treemaplayer"/><g class="sunburstlayer"/><g class="glimages"/><defs id="topdefs-ab296a"><g class="clips"/><clipPath id="legendab296a"><rect width="458" height="29" x="0" y="0"/></clipPath></defs><g class="layer-above"><g class="imagelayer"/><g class="shapelayer"/></g><g class="infolayer"><g class="legend" pointer-events="all" transform="translate(212, 11.519999999999996)"><rect class="bg" shape-rendering="crispEdges" style="stroke: rgb(68, 68, 68); stroke-opacity: 1; fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 0px;" width="458" height="29" x="0" y="0"/><g class="scrollbox" transform="translate(0, 0)" clip-path="url('#legendab296a')"><g class="groups"><g class="traces" transform="translate(0, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Close</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"><path class="js-line" d="M5,0h30" style="fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px;"/></g><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="74.859375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(77.359375, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Entry</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="74.640625" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(154.5, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Exit - Profit</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="110.984375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(267.984375, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Exit - Loss</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="105.578125" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(376.0625, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Active</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="79.234375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g></g></g><rect class="scrollbar" rx="20" ry="3" width="0" height="0" style="fill: rgb(128, 139, 164); fill-opacity: 1;" x="0" y="0"/></g><g class="g-gtitle"/><g class="g-xtitle"/><g class="g-ytitle"/></g></svg>
\ No newline at end of file
+<svg class="main-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="700" height="350" style="" viewBox="0 0 700 350"><rect x="0" y="0" width="700" height="350" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/><defs id="defs-f21b1f"><g class="clips"><clipPath id="clipf21b1fxyplot" class="plotclip"><rect width="637" height="261"/></clipPath><clipPath class="axesclip" id="clipf21b1fx"><rect x="33" y="0" width="637" height="350"/></clipPath><clipPath class="axesclip" id="clipf21b1fy"><rect x="0" y="46" width="700" height="261"/></clipPath><clipPath class="axesclip" id="clipf21b1fxy"><rect x="33" y="46" width="637" height="261"/></clipPath></g><g class="gradients"/><g class="patterns"/></defs><g class="bglayer"><rect class="bg" x="33" y="46" width="637" height="261" style="fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;"/></g><g class="layer-below"><g class="imagelayer"/><g class="shapelayer"/></g><g class="cartesianlayer"><g class="subplot xy"><g class="layer-subplot"><g class="shapelayer"><path data-index="0" fill-rule="evenodd" d="M69.22999999999999,289.58000000000004H163.32V214.19H69.22999999999999Z" clip-path="url(#clipf21b1fxy)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(0, 128, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="1" fill-rule="evenodd" d="M69.22999999999999,289.58000000000004H257.40999999999997V138.81H69.22999999999999Z" clip-path="url(#clipf21b1fxy)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(0, 128, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="2" fill-rule="evenodd" d="M351.5,63.43H445.59V138.81H351.5Z" clip-path="url(#clipf21b1fxy)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 0, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="3" fill-rule="evenodd" d="M351.5,63.43H539.6800000000001V214.19H351.5Z" clip-path="url(#clipf21b1fxy)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 0, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="4" fill-rule="evenodd" d="M351.5,63.43H633.77V289.58000000000004H351.5Z" clip-path="url(#clipf21b1fxy)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 0, 0); fill-opacity: 1; stroke-width: 0px;"/><path data-index="5" fill-rule="evenodd" d="M351.5,63.43H633.77V289.58000000000004H351.5Z" clip-path="url(#clipf21b1fxy)" style="opacity: 0.2; stroke: rgb(0, 0, 0); stroke-opacity: 0; fill: rgb(255, 0, 0); fill-opacity: 1; stroke-width: 0px;"/></g><g class="imagelayer"/></g><g class="gridlayer"><g class="x"><path class="xgrid crisp" transform="translate(69.22999999999999,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(163.32,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(257.40999999999997,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(351.5,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(445.59,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(539.6800000000001,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(633.77,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g><g class="y"><path class="ygrid crisp" transform="translate(0,289.58000000000004)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,251.88)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,214.19)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,176.5)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,138.81)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,101.12)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,63.43)" d="M33,0h637" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g></g><g class="zerolinelayer"/><path class="xlines-below"/><path class="ylines-below"/><g class="overlines-below"/><g class="xaxislayer-below"/><g class="yaxislayer-below"/><g class="overaxes-below"/><g class="plot" transform="translate(33,46)" clip-path="url(#clipf21b1fxyplot)"><g class="scatterlayer mlayer"><g class="trace scatter tracefbf947d9-9fc9-4263-a704-e69999472392" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"><path class="js-line" d="M36.23,243.58L318.5,17.43L412.59,92.81L600.77,243.58" style="vector-effect: non-scaling-stroke; fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px; opacity: 1;"/></g><g class="points"><path class="point" transform="translate(36.23,243.58)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(130.32,168.19)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(224.41,92.81)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(318.5,17.43)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(412.59,92.81)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(506.68,168.19)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/><path class="point" transform="translate(600.77,243.58)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace04ff3989-5830-4a44-949e-a4366b891a6d" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(36.23,243.58)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(36.23,243.58)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(318.5,17.43)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(318.5,17.43)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(318.5,17.43)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(318.5,17.43)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter tracea99c6e88-b30e-485e-b86c-8444dd6576bd" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(130.32,168.19)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(224.41,92.81)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter traceba19e2a9-bc0a-44e0-a222-e8dc43accd67" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(412.59,92.81)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(506.68,168.19)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(600.77,243.58)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace42795de0-0ea9-4b54-bdf8-46c7a842d65b" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(600.77,243.58)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g><g class="text"/></g></g></g><g class="overplot"/><path class="xlines-above crisp" d="M0,0" style="fill: none;"/><path class="ylines-above crisp" d="M0,0" style="fill: none;"/><g class="overlines-above"/><g class="xaxislayer-above"><g class="xtick"><text text-anchor="middle" x="0" y="320" transform="translate(69.22999999999999,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;"><tspan class="line" dy="0em" x="0" y="320">Jan 1</tspan><tspan class="line" dy="1.3em" x="0" y="320">2020</tspan></text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(163.32,0)">Jan 2</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(257.40999999999997,0)">Jan 3</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(351.5,0)">Jan 4</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(445.59,0)">Jan 5</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(539.6800000000001,0)">Jan 6</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(633.77,0)">Jan 7</text></g></g><g class="yaxislayer-above"><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" transform="translate(0,289.58000000000004)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;">1</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,251.88)">1.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,214.19)">2</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,176.5)">2.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,138.81)">3</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,101.12)">3.5</text></g><g class="ytick"><text text-anchor="end" x="32" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,63.43)">4</text></g></g><g class="overaxes-above"/></g></g><g class="polarlayer"/><g class="ternarylayer"/><g class="geolayer"/><g class="funnelarealayer"/><g class="pielayer"/><g class="iciclelayer"/><g class="treemaplayer"/><g class="sunburstlayer"/><g class="glimages"/><defs id="topdefs-f21b1f"><g class="clips"/><clipPath id="legendf21b1f"><rect width="455" height="29" x="0" y="0"/></clipPath></defs><g class="layer-above"><g class="imagelayer"/><g class="shapelayer"/></g><g class="infolayer"><g class="legend" pointer-events="all" transform="translate(215,11.779999999999994)"><rect class="bg" shape-rendering="crispEdges" style="stroke: rgb(68, 68, 68); stroke-opacity: 1; fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 0px;" width="455" height="29" x="0" y="0"/><g class="scrollbox" transform="" clip-path="url(#legendf21b1f)"><g class="groups"><g class="traces" transform="translate(0,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Price</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"><path class="js-line" d="M5,0h30" style="fill: none; stroke: rgb(31, 119, 180); stroke-opacity: 1; stroke-width: 2px;"/></g><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3,0A3,3 0 1,1 0,-3A3,3 0 0,1 3,0Z" style="opacity: 1; stroke-width: 0px; fill: rgb(31, 119, 180); fill-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="71.5625" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(74.0625,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Entry</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(66, 133, 244); fill-opacity: 1; stroke: rgb(11, 84, 205); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="74.640625" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(151.203125,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Exit - Profit</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="110.984375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(264.6875,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Exit - Loss</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="105.578125" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(372.765625,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Active</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M3.5,3.5H-3.5V-3.5H3.5Z" style="opacity: 1; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="79.234375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g></g></g><rect class="scrollbar" rx="20" ry="3" width="0" height="0" style="fill: rgb(128, 139, 164); fill-opacity: 1;" x="0" y="0"/></g><g class="g-gtitle"/><g class="g-xtitle"/><g class="g-ytitle"/></g></svg>
\ No newline at end of file
diff --git a/docs/img/trades_plot_pnl.svg b/docs/img/trades_plot_pnl.svg
index 6ce43071..44f75c5e 100644
--- a/docs/img/trades_plot_pnl.svg
+++ b/docs/img/trades_plot_pnl.svg
@@ -1,1 +1,1 @@
-<svg class="main-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="700" height="350" style="" viewBox="0 0 700 350"><rect x="0" y="0" width="700" height="350" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/><defs id="defs-b3cdb1"><g class="clips"><clipPath id="clipb3cdb1xyplot" class="plotclip"><rect width="628" height="274"/></clipPath><clipPath class="axesclip" id="clipb3cdb1x"><rect x="42" y="0" width="628" height="350"/></clipPath><clipPath class="axesclip" id="clipb3cdb1y"><rect x="0" y="46" width="700" height="274"/></clipPath><clipPath class="axesclip" id="clipb3cdb1xy"><rect x="42" y="46" width="628" height="274"/></clipPath></g><g class="gradients"/></defs><g class="bglayer"><rect class="bg" x="42" y="46" width="628" height="274" style="fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;"/></g><g class="layer-below"><g class="imagelayer"/><g class="shapelayer"/></g><g class="cartesianlayer"><g class="subplot xy"><g class="layer-subplot"><g class="shapelayer"/><g class="imagelayer"/></g><g class="gridlayer"><g class="x"><path class="xgrid crisp" transform="translate(79.65,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(190.32,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(300.98,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(411.65,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(522.31,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(632.98,0)" d="M0,46v274" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g><g class="y"><path class="ygrid crisp" transform="translate(0,300.68)" d="M42,0h628" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,254.23)" d="M42,0h628" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,207.79)" d="M42,0h628" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,114.9)" d="M42,0h628" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,68.45)" d="M42,0h628" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g></g><g class="zerolinelayer"><path class="yzl zl crisp" transform="translate(0,161.34)" d="M42,0h628" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 2px;"/></g><path class="xlines-below"/><path class="ylines-below"/><g class="overlines-below"/><g class="xaxislayer-below"/><g class="yaxislayer-below"/><g class="overaxes-below"/><g class="plot" transform="translate(42, 46)" clip-path="url('#clipb3cdb1xyplot')"><g class="scatterlayer mlayer"><g class="trace scatter trace144827fe-cfb8-415f-86e2-c4c68c0e7f98" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(37.65,68.9)" d="M5,0A5,5 0 1,1 0,-5A5,5 0 0,1 5,0Z" style="opacity: 0.814286; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(148.32,22.45)" d="M7,0A7,7 0 1,1 0,-7A7,7 0 0,1 7,0Z" style="opacity: 0.9; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace26fdfb1a-20e4-419a-977d-9806c418364c" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(369.65,161.79)" d="M3.5,0A3.5,3.5 0 1,1 0,-3.5A3.5,3.5 0 0,1 3.5,0Z" style="opacity: 0.75; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(480.31,208.23)" d="M4,0A4,4 0 1,1 0,-4A4,4 0 0,1 4,0Z" style="opacity: 0.771429; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(590.98,254.68)" d="M4.5,0A4.5,4.5 0 1,1 0,-4.5A4.5,4.5 0 0,1 4.5,0Z" style="opacity: 0.792857; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace0079c865-9212-424c-a405-b2b76c7713a3" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(590.98,254.68)" d="M4.5,0A4.5,4.5 0 1,1 0,-4.5A4.5,4.5 0 0,1 4.5,0Z" style="opacity: 0.792857; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g><g class="text"/></g></g></g><g class="overplot"/><path class="xlines-above crisp" d="M0,0" style="fill: none;"/><path class="ylines-above crisp" d="M0,0" style="fill: none;"/><g class="overlines-above"/><g class="xaxislayer-above"><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(79.65,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(190.32,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">2</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(300.98,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">3</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(411.65,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">4</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(522.31,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">5</text></g><g class="xtick"><text text-anchor="middle" x="0" y="333" transform="translate(632.98,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">6</text></g></g><g class="yaxislayer-above"><g class="ytick"><text text-anchor="end" x="41" y="4.199999999999999" transform="translate(0,300.68)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">−1.5</text></g><g class="ytick"><text text-anchor="end" x="41" y="4.199999999999999" transform="translate(0,254.23)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">−1</text></g><g class="ytick"><text text-anchor="end" x="41" y="4.199999999999999" transform="translate(0,207.79)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">−0.5</text></g><g class="ytick"><text text-anchor="end" x="41" y="4.199999999999999" transform="translate(0,161.34)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">0</text></g><g class="ytick"><text text-anchor="end" x="41" y="4.199999999999999" transform="translate(0,114.9)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">0.5</text></g><g class="ytick"><text text-anchor="end" x="41" y="4.199999999999999" transform="translate(0,68.45)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">1</text></g></g><g class="overaxes-above"/></g></g><g class="polarlayer"/><g class="ternarylayer"/><g class="geolayer"/><g class="funnelarealayer"/><g class="pielayer"/><g class="treemaplayer"/><g class="sunburstlayer"/><g class="glimages"/><defs id="topdefs-b3cdb1"><g class="clips"/><clipPath id="legendb3cdb1"><rect width="333" height="29" x="0" y="0"/></clipPath></defs><g class="layer-above"><g class="imagelayer"/><g class="shapelayer"><path data-index="0" fill-rule="evenodd" d="M42,161.34L670,161.34" clip-path="url('#clipb3cdb1y')" style="opacity: 1; stroke: rgb(128, 128, 128); stroke-opacity: 1; fill: rgb(0, 0, 0); fill-opacity: 0; stroke-dasharray: 9px, 9px; stroke-width: 2px;"/></g></g><g class="infolayer"><g class="legend" pointer-events="all" transform="translate(337, 11.519999999999996)"><rect class="bg" shape-rendering="crispEdges" style="stroke: rgb(68, 68, 68); stroke-opacity: 1; fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 0px;" width="333" height="29" x="0" y="0"/><g class="scrollbox" transform="translate(0, 0)" clip-path="url('#legendb3cdb1')"><g class="groups"><g class="traces" transform="translate(0, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Closed - Profit</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M6,0A6,6 0 1,1 0,-6A6,6 0 0,1 6,0Z" style="opacity: 0.857143; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="128.109375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(130.609375, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Closed - Loss</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4,0A4,4 0 1,1 0,-4A4,4 0 0,1 4,0Z" style="opacity: 0.771429; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="122.6875" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(255.796875, 14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Open</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.5,0A4.5,4.5 0 1,1 0,-4.5A4.5,4.5 0 0,1 4.5,0Z" style="opacity: 0.792857; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="74.171875" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g></g></g><rect class="scrollbar" rx="20" ry="3" width="0" height="0" style="fill: rgb(128, 139, 164); fill-opacity: 1;" x="0" y="0"/></g><g class="g-gtitle"/><g class="g-xtitle"/><g class="g-ytitle"/></g></svg>
\ No newline at end of file
+<svg class="main-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="700" height="350" style="" viewBox="0 0 700 350"><rect x="0" y="0" width="700" height="350" style="fill: rgb(255, 255, 255); fill-opacity: 1;"/><defs id="defs-b1f952"><g class="clips"><clipPath id="clipb1f952xyplot" class="plotclip"><rect width="599" height="261"/></clipPath><clipPath class="axesclip" id="clipb1f952x"><rect x="71" y="0" width="599" height="350"/></clipPath><clipPath class="axesclip" id="clipb1f952y"><rect x="0" y="46" width="700" height="261"/></clipPath><clipPath class="axesclip" id="clipb1f952xy"><rect x="71" y="46" width="599" height="261"/></clipPath></g><g class="gradients"/><g class="patterns"/></defs><g class="bglayer"><rect class="bg" x="71" y="46" width="599" height="261" style="fill: rgb(229, 236, 246); fill-opacity: 1; stroke-width: 0;"/></g><g class="layer-below"><g class="imagelayer"/><g class="shapelayer"/></g><g class="cartesianlayer"><g class="subplot xy"><g class="layer-subplot"><g class="shapelayer"/><g class="imagelayer"/></g><g class="gridlayer"><g class="x"><path class="xgrid crisp" transform="translate(107.2,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(212.65,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(318.09000000000003,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(423.53,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(528.98,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="xgrid crisp" transform="translate(634.42,0)" d="M0,46v261" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g><g class="y"><path class="ygrid crisp" transform="translate(0,268.28)" d="M71,0h599" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,188.09)" d="M71,0h599" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,147.99)" d="M71,0h599" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,107.9)" d="M71,0h599" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/><path class="ygrid crisp" transform="translate(0,67.8)" d="M71,0h599" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 1px;"/></g></g><g class="zerolinelayer"><path class="yzl zl crisp" transform="translate(0,228.18)" d="M71,0h599" style="stroke: rgb(255, 255, 255); stroke-opacity: 1; stroke-width: 2px;"/></g><path class="xlines-below"/><path class="ylines-below"/><g class="overlines-below"/><g class="xaxislayer-below"/><g class="yaxislayer-below"/><g class="overaxes-below"/><g class="plot" transform="translate(71,46)" clip-path="url(#clipb1f952xyplot)"><g class="scatterlayer mlayer"><g class="trace scatter traced06dba6b-a6f0-4d02-bd0c-cfd3b4f2ad48" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(36.2,101.99)" d="M5,0A5,5 0 1,1 0,-5A5,5 0 0,1 5,0Z" style="opacity: 0.814286; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(141.65,21.8)" d="M7,0A7,7 0 1,1 0,-7A7,7 0 0,1 7,0Z" style="opacity: 0.9; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter trace1423324f-9bd4-4dd2-9725-022120fbde4c" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(352.53,202.23)" d="M3.5,0A3.5,3.5 0 1,1 0,-3.5A3.5,3.5 0 0,1 3.5,0Z" style="opacity: 0.75; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(457.98,222.28)" d="M4,0A4,4 0 1,1 0,-4A4,4 0 0,1 4,0Z" style="opacity: 0.771429; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/><path class="point plotly-customdata" transform="translate(563.42,242.33)" d="M4.5,0A4.5,4.5 0 1,1 0,-4.5A4.5,4.5 0 0,1 4.5,0Z" style="opacity: 0.792857; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g><g class="text"/></g><g class="trace scatter tracea7c70884-137d-4065-a485-957193af2fc6" style="stroke-miterlimit: 2; opacity: 1;"><g class="fills"/><g class="errorbars"/><g class="lines"/><g class="points"><path class="point plotly-customdata" transform="translate(563.42,242.33)" d="M4.5,0A4.5,4.5 0 1,1 0,-4.5A4.5,4.5 0 0,1 4.5,0Z" style="opacity: 0.792857; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g><g class="text"/></g></g></g><g class="overplot"/><path class="xlines-above crisp" d="M0,0" style="fill: none;"/><path class="ylines-above crisp" d="M0,0" style="fill: none;"/><g class="overlines-above"/><g class="xaxislayer-above"><g class="xtick"><text text-anchor="middle" x="0" y="320" transform="translate(107.2,0)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;"><tspan class="line" dy="0em" x="0" y="320">Jan 2</tspan><tspan class="line" dy="1.3em" x="0" y="320">2020</tspan></text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(212.65,0)">Jan 3</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(318.09000000000003,0)">Jan 4</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(423.53,0)">Jan 5</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(528.98,0)">Jan 6</text></g><g class="xtick"><text text-anchor="middle" x="0" y="320" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(634.42,0)">Jan 7</text></g></g><g class="yaxislayer-above"><g class="ytick"><text text-anchor="end" x="70" y="4.199999999999999" transform="translate(0,268.28)" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;">−50.00%</text></g><g class="ytick"><text text-anchor="end" x="70" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,228.18)">0.00%</text></g><g class="ytick"><text text-anchor="end" x="70" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,188.09)">50.00%</text></g><g class="ytick"><text text-anchor="end" x="70" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,147.99)">100.00%</text></g><g class="ytick"><text text-anchor="end" x="70" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,107.9)">150.00%</text></g><g class="ytick"><text text-anchor="end" x="70" y="4.199999999999999" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre; opacity: 1;" transform="translate(0,67.8)">200.00%</text></g></g><g class="overaxes-above"/></g></g><g class="polarlayer"/><g class="ternarylayer"/><g class="geolayer"/><g class="funnelarealayer"/><g class="pielayer"/><g class="iciclelayer"/><g class="treemaplayer"/><g class="sunburstlayer"/><g class="glimages"/><defs id="topdefs-b1f952"><g class="clips"/><clipPath id="legendb1f952"><rect width="333" height="29" x="0" y="0"/></clipPath></defs><g class="layer-above"><g class="imagelayer"/><g class="shapelayer"><path data-index="0" fill-rule="evenodd" d="M71,228.18L670,228.18" clip-path="url(#clipb1f952y)" style="opacity: 1; stroke: rgb(128, 128, 128); stroke-opacity: 1; fill: rgb(0, 0, 0); fill-opacity: 0; stroke-dasharray: 9px, 9px; stroke-width: 2px;"/></g></g><g class="infolayer"><g class="legend" pointer-events="all" transform="translate(337,11.779999999999994)"><rect class="bg" shape-rendering="crispEdges" style="stroke: rgb(68, 68, 68); stroke-opacity: 1; fill: rgb(255, 255, 255); fill-opacity: 1; stroke-width: 0px;" width="333" height="29" x="0" y="0"/><g class="scrollbox" transform="" clip-path="url(#legendb1f952)"><g class="groups"><g class="traces" transform="translate(0,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Closed - Profit</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M6,0A6,6 0 1,1 0,-6A6,6 0 0,1 6,0Z" style="opacity: 0.857143; stroke-width: 1px; fill: rgb(55, 177, 63); fill-opacity: 1; stroke: rgb(38, 123, 44); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="128.109375" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(130.609375,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Closed - Loss</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4,0A4,4 0 1,1 0,-4A4,4 0 0,1 4,0Z" style="opacity: 0.771429; stroke-width: 1px; fill: rgb(234, 67, 53); fill-opacity: 1; stroke: rgb(181, 31, 18); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="122.6875" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g><g class="traces" transform="translate(255.796875,14.5)" style="opacity: 1;"><text class="legendtext" text-anchor="start" x="40" y="4.680000000000001" style="font-family: 'Open Sans', verdana, arial, sans-serif; font-size: 12px; fill: rgb(42, 63, 95); fill-opacity: 1; white-space: pre;">Open</text><g class="layers" style="opacity: 1;"><g class="legendfill"/><g class="legendlines"/><g class="legendsymbols"><g class="legendpoints"><path class="scatterpts" transform="translate(20,0)" d="M4.5,0A4.5,4.5 0 1,1 0,-4.5A4.5,4.5 0 0,1 4.5,0Z" style="opacity: 0.792857; stroke-width: 1px; fill: rgb(255, 170, 0); fill-opacity: 1; stroke: rgb(178, 118, 0); stroke-opacity: 1;"/></g></g></g><rect class="legendtoggle" x="0" y="-9.5" width="74.171875" height="19" style="fill: rgb(0, 0, 0); fill-opacity: 0;"/></g></g></g><rect class="scrollbar" rx="20" ry="3" width="0" height="0" style="fill: rgb(128, 139, 164); fill-opacity: 1;" x="0" y="0"/></g><g class="g-gtitle"/><g class="g-xtitle"/><g class="g-ytitle"/></g></svg>
\ No newline at end of file
diff --git a/examples/PairsTrading.ipynb b/examples/PairsTrading.ipynb
index e7ac5646..6e65a3da 100644
--- a/examples/PairsTrading.ipynb
+++ b/examples/PairsTrading.ipynb
@@ -1006,7 +1006,7 @@
       "Total Trades                                   10\n",
       "Total Closed Trades                             8\n",
       "Total Open Trades                               2\n",
-      "Open Trade P&L                         149.652774\n",
+      "Open Trade PnL                         149.652774\n",
       "Win Rate [%]                                 62.5\n",
       "Best Trade [%]                           9.267971\n",
       "Worst Trade [%]                         -9.229397\n",
@@ -1446,7 +1446,7 @@
       "Total Trades                                   10\n",
       "Total Closed Trades                             8\n",
       "Total Open Trades                               2\n",
-      "Open Trade P&L                         149.652774\n",
+      "Open Trade PnL                         149.652774\n",
       "Win Rate [%]                                 62.5\n",
       "Best Trade [%]                           9.267971\n",
       "Worst Trade [%]                         -9.229397\n",
diff --git a/examples/PortfolioOptimization.ipynb b/examples/PortfolioOptimization.ipynb
index 8d2a5179..5ec6d7b2 100644
--- a/examples/PortfolioOptimization.ipynb
+++ b/examples/PortfolioOptimization.ipynb
@@ -424,7 +424,7 @@
       "Total Trades                                          5\n",
       "Total Closed Trades                                   0\n",
       "Total Open Trades                                     5\n",
-      "Open Trade P&L                               143.693254\n",
+      "Open Trade PnL                               143.693254\n",
       "Win Rate [%]                                        NaN\n",
       "Best Trade [%]                                      NaN\n",
       "Worst Trade [%]                                     NaN\n",
@@ -604,7 +604,7 @@
       "Total Trades                                           92\n",
       "Total Closed Trades                                    87\n",
       "Total Open Trades                                       5\n",
-      "Open Trade P&L                                  117.47751\n",
+      "Open Trade PnL                                  117.47751\n",
       "Win Rate [%]                                    97.701149\n",
       "Best Trade [%]                                 189.769868\n",
       "Worst Trade [%]                                  -4.76643\n",
@@ -839,7 +839,7 @@
       "Total Trades                                           67\n",
       "Total Closed Trades                                    62\n",
       "Total Open Trades                                       5\n",
-      "Open Trade P&L                                   33.96542\n",
+      "Open Trade PnL                                   33.96542\n",
       "Win Rate [%]                                    77.419355\n",
       "Best Trade [%]                                  43.780282\n",
       "Worst Trade [%]                                -15.059923\n",
@@ -950,7 +950,7 @@
       "Total Trades                                           39\n",
       "Total Closed Trades                                    34\n",
       "Total Open Trades                                       5\n",
-      "Open Trade P&L                                  10.464974\n",
+      "Open Trade PnL                                  10.464974\n",
       "Win Rate [%]                                    58.823529\n",
       "Best Trade [%]                                  16.361723\n",
       "Worst Trade [%]                                -15.501637\n",
@@ -1115,7 +1115,7 @@
       "Total Trades                                          3\n",
       "Total Closed Trades                                   0\n",
       "Total Open Trades                                     3\n",
-      "Open Trade P&L                                 159.6377\n",
+      "Open Trade PnL                                 159.6377\n",
       "Win Rate [%]                                        NaN\n",
       "Best Trade [%]                                      NaN\n",
       "Worst Trade [%]                                     NaN\n",
@@ -1261,7 +1261,7 @@
       "Total Trades                                         44\n",
       "Total Closed Trades                                  41\n",
       "Total Open Trades                                     3\n",
-      "Open Trade P&L                                27.442025\n",
+      "Open Trade PnL                                27.442025\n",
       "Win Rate [%]                                   78.04878\n",
       "Best Trade [%]                                35.953902\n",
       "Worst Trade [%]                               -30.63541\n",
diff --git a/examples/PortingBTStrategy.ipynb b/examples/PortingBTStrategy.ipynb
index b802092c..406d241d 100644
--- a/examples/PortingBTStrategy.ipynb
+++ b/examples/PortingBTStrategy.ipynb
@@ -733,7 +733,7 @@
       "Total Trades                                         40\n",
       "Total Closed Trades                                  39\n",
       "Total Open Trades                                     1\n",
-      "Open Trade P&L                                -1.496004\n",
+      "Open Trade PnL                                -1.496004\n",
       "Win Rate [%]                                  69.230769\n",
       "Best Trade [%]                                 2.153345\n",
       "Worst Trade [%]                               -2.796592\n",
diff --git a/static/bbands.gif b/static/bbands.gif
index 69727c40..787e415d 100644
Binary files a/static/bbands.gif and b/static/bbands.gif differ
diff --git a/vectorbt/__init__.py b/vectorbt/__init__.py
index cd9c43c9..a5c1e777 100644
--- a/vectorbt/__init__.py
+++ b/vectorbt/__init__.py
@@ -457,7 +457,7 @@
 >>> mean_return.vbt.barplot(xaxis_title='Symbol', yaxis_title='Mean total return')
 ```
 
-![](/vectorbt/docs/img/index_by_symbol.svg)
+![](/docs/img/index_by_symbol.svg)
 
 Not only strategies and instruments can act as separate features, but also time. If we want to find out
 when our strategy performs best, it's reasonable to test it over multiple time periods. vectorbt allows
@@ -516,7 +516,7 @@ class method in order to be able to compute performance metrics such as the Shar
 ...     legend_title_text='Symbol')
 ```
 
-![](/vectorbt/docs/img/index_by_any.svg)
+![](/docs/img/index_by_any.svg)
 
 There is much more to backtesting than simply stacking columns: vectorbt offers functions for
 most parts of a backtesting pipeline, from building indicators and generating signals, to
diff --git a/vectorbt/_settings.py b/vectorbt/_settings.py
index 67f8e2a5..3cb0ecf9 100644
--- a/vectorbt/_settings.py
+++ b/vectorbt/_settings.py
@@ -306,6 +306,9 @@ def reset_theme(self) -> None:
                 )
             )
         ),
+        ranges=dict(
+            stats=Config()  # flex
+        ),
         drawdowns=dict(
             stats=Config(  # flex
                 dict(
@@ -401,21 +404,18 @@ def reset_theme(self) -> None:
         trades=dict(
             stats=Config(  # flex
                 dict(
-                    filters=dict(
-                        is_positions=dict(
-                            filter_func=lambda self, metric_settings: self.is_positions
-                        )
-                    ),
                     settings=dict(
                         incl_open=False
                     ),
                     template_mapping=dict(
-                        trades_tag=RepEval("'positions' if self.is_positions else 'trades'"),
                         incl_open_tags=RepEval("['open', 'closed'] if incl_open else ['closed']")
                     )
                 )
             )
         ),
+        positions=dict(
+            stats=Config()  # flex
+        ),
         logs=dict(
             stats=Config()  # flex
         ),
@@ -482,21 +482,12 @@ def reset_theme(self) -> None:
             ),
             plot=Config(  # flex
                 dict(
-                    subplots=['orders', 'trade_returns', 'cum_returns'],
-                    filters=dict(
-                        has_year_freq=dict(
-                            filter_func=lambda self, subplot_settings:
-                            subplot_settings['year_freq'] is not None,
-                            warning_message=Sub("Subplot '$subplot_name' requires year frequency to be set")
-                        )
-                    ),
+                    subplots=['orders', 'trade_pnl', 'cum_returns'],
                     settings=dict(
                         use_asset_returns=False,
-                        use_positions=False,
                         incl_open=True
                     ),
                     template_mapping=dict(
-                        trades_tag=RepEval("'positions' if use_positions else 'trades'"),
                         incl_open_tags=RepEval("['open', 'closed'] if incl_open else ['closed']")
                     )
                 )
@@ -644,6 +635,14 @@ def reset_theme(self) -> None:
 {settings['generic'].to_doc()}
 ```
 
+## settings.generic.ranges
+
+Settings applied across `vectorbt.generic.ranges`.
+
+```json
+{settings['ranges'].to_doc()}
+```
+
 ## settings.generic.drawdowns
 
 Settings applied across `vectorbt.generic.drawdowns`.
diff --git a/vectorbt/_typing.py b/vectorbt/_typing.py
index d29f147c..f1b846bf 100644
--- a/vectorbt/_typing.py
+++ b/vectorbt/_typing.py
@@ -133,9 +133,6 @@ class SupportsTZInfo(Protocol):
 
 # Signals
 ChoiceFunc = Callable[[int, int, int, VarArg()], Array1d]
-RangeMapMetaOutput = Tuple[Array1d, Array1d, Array1d]
-RangeMapMetaFunc = Callable[[VarArg()], RangeMapMetaOutput]
-RangeMapFunc = Callable[[int, int, int, VarArg()], float]
 RankFunc = Callable[[int, int, int, int, int, VarArg()], int]
 
 # Records
@@ -152,4 +149,4 @@ class SupportsTZInfo(Protocol):
 
 # Mappings
 Enum = NamedTuple
-MappingLike = Union[Mapping, Enum, IndexLike]
+MappingLike = Union[str, Mapping, Enum, IndexLike]
diff --git a/vectorbt/base/accessors.py b/vectorbt/base/accessors.py
index d6a6d4a0..0000f5d5 100644
--- a/vectorbt/base/accessors.py
+++ b/vectorbt/base/accessors.py
@@ -65,7 +65,7 @@
 
 from vectorbt import _typing as tp
 from vectorbt.utils import checks
-from vectorbt.utils.decorators import class_or_instancemethod, add_binary_magic_methods, add_unary_magic_methods
+from vectorbt.utils.decorators import class_or_instancemethod, attach_binary_magic_methods, attach_unary_magic_methods
 from vectorbt.utils.config import merge_dicts, get_func_arg_names
 from vectorbt.base import combine_fns, index_fns, reshape_fns
 from vectorbt.base.column_grouper import ColumnGrouper
@@ -74,8 +74,8 @@
 BaseAccessorT = tp.TypeVar("BaseAccessorT", bound="BaseAccessor")
 
 
-@add_binary_magic_methods(lambda self, other, np_func: self.combine(other, allow_multiple=False, combine_func=np_func))
-@add_unary_magic_methods(lambda self, np_func: self.apply(apply_func=np_func))
+@attach_binary_magic_methods(lambda self, other, np_func: self.combine(other, allow_multiple=False, combine_func=np_func))
+@attach_unary_magic_methods(lambda self, np_func: self.apply(apply_func=np_func))
 class BaseAccessor(Wrapping):
     """Accessor on top of Series and DataFrames.
 
@@ -88,7 +88,7 @@ class BaseAccessor(Wrapping):
     `**kwargs` will be passed to `vectorbt.base.array_wrapper.ArrayWrapper`."""
 
     def __init__(self, obj: tp.SeriesFrame, wrapper: tp.Optional[ArrayWrapper] = None, **kwargs) -> None:
-        checks.assert_type(obj, (pd.Series, pd.DataFrame))
+        checks.assert_instance_of(obj, (pd.Series, pd.DataFrame))
 
         self._obj = obj
 
@@ -342,7 +342,7 @@ def align_to(self, other: tp.SeriesFrame, wrap_kwargs: tp.KwargsLike = None) ->
         y  3  4  3  4
         ```
         """
-        checks.assert_type(other, (pd.Series, pd.DataFrame))
+        checks.assert_instance_of(other, (pd.Series, pd.DataFrame))
         obj = reshape_fns.to_2d(self.obj)
         other = reshape_fns.to_2d(other)
 
@@ -711,7 +711,7 @@ class BaseSRAccessor(BaseAccessor):
     Accessible through `pd.Series.vbt` and all child accessors."""
 
     def __init__(self, obj: tp.Series, **kwargs) -> None:
-        checks.assert_type(obj, pd.Series)
+        checks.assert_instance_of(obj, pd.Series)
 
         BaseAccessor.__init__(self, obj, **kwargs)
 
@@ -730,7 +730,7 @@ class BaseDFAccessor(BaseAccessor):
     Accessible through `pd.DataFrame.vbt` and all child accessors."""
 
     def __init__(self, obj: tp.Frame, **kwargs) -> None:
-        checks.assert_type(obj, pd.DataFrame)
+        checks.assert_instance_of(obj, pd.DataFrame)
 
         BaseAccessor.__init__(self, obj, **kwargs)
 
diff --git a/vectorbt/base/array_wrapper.py b/vectorbt/base/array_wrapper.py
index e845810b..5106e9bd 100644
--- a/vectorbt/base/array_wrapper.py
+++ b/vectorbt/base/array_wrapper.py
@@ -341,6 +341,9 @@ def from_obj(cls: tp.Type[ArrayWrapperT], obj: tp.ArrayLike, *args, **kwargs) ->
         index = index_fns.get_index(pd_obj, 0)
         columns = index_fns.get_index(pd_obj, 1)
         ndim = pd_obj.ndim
+        kwargs.pop('index', None)
+        kwargs.pop('columns', None)
+        kwargs.pop('ndim', None)
         return cls(index, columns, ndim, *args, **kwargs)
 
     @classmethod
@@ -427,8 +430,8 @@ def freq(self) -> tp.Optional[pd.Timedelta]:
                 return freq_to_timedelta(self.index.inferred_freq)
         return freq
 
-    def to_timedelta(self, a: tp.MaybeArray[float],
-                    silence_warnings: tp.Optional[bool] = None) -> tp.Union[pd.Timedelta, tp.Array]:
+    def to_timedelta(self, a: tp.MaybeArray[float], to_pd: bool = False,
+                     silence_warnings: tp.Optional[bool] = None) -> tp.Union[pd.Timedelta, np.timedelta64, tp.Array]:
         """Convert array to duration using `ArrayWrapper.freq`."""
         from vectorbt._settings import settings
         array_wrapper_cfg = settings['array_wrapper']
@@ -441,6 +444,8 @@ def to_timedelta(self, a: tp.MaybeArray[float],
                 warnings.warn("Couldn't parse the frequency of index. Pass it as `freq` or "
                               "define it globally under `settings.array_wrapper`.", stacklevel=2)
             return a
+        if to_pd:
+            return pd.to_timedelta(a * self.freq)
         return a * self.freq
 
     @property
@@ -507,8 +512,13 @@ def wrap(self,
              silence_warnings: tp.Optional[bool] = False) -> tp.SeriesFrame:
         """Wrap a NumPy array using the stored metadata.
 
-        !!! note
-            Array will be converted to a NumPy array before wrapping."""
+        Runs the following pipeline:
+
+        1) Converts to NumPy array
+        2) Fills NaN (optional)
+        3) Wraps using index, columns, and dtype (optional)
+        4) Converts to index (optional)
+        5) Converts to timedelta using `ArrayWrapper.to_timedelta` (optional)"""
         from vectorbt._settings import settings
         array_wrapper_cfg = settings['array_wrapper']
 
@@ -577,7 +587,7 @@ def wrap_reduced(self,
         or the index of the resulting series/dataframe if reducing to an array per column.
         `columns` can be set to override object's default columns.
 
-        If `to_timedelta` is set, calls `ArrayWrapper.to_timedelta`."""
+        See `ArrayWrapper.wrap` for the pipeline."""
         from vectorbt._settings import settings
         array_wrapper_cfg = settings['array_wrapper']
 
@@ -656,6 +666,16 @@ def dummy(self, group_by: tp.GroupByLike = None, **kwargs) -> tp.SeriesFrame:
         _self = self.resolve(group_by=group_by)
         return _self.wrap(np.empty(_self.shape), **kwargs)
 
+    def fill(self, fill_value: tp.Scalar, group_by: tp.GroupByLike = None, **kwargs) -> tp.SeriesFrame:
+        """Fill a Series/DataFrame."""
+        _self = self.resolve(group_by=group_by)
+        return _self.wrap(np.full(_self.shape_2d, fill_value), **kwargs)
+
+    def fill_reduced(self, fill_value: tp.Scalar, group_by: tp.GroupByLike = None, **kwargs) -> tp.SeriesFrame:
+        """Fill a reduced Series/DataFrame."""
+        _self = self.resolve(group_by=group_by)
+        return _self.wrap(np.full(_self.shape_2d[1], fill_value), **kwargs)
+
 
 WrappingT = tp.TypeVar("WrappingT", bound="Wrapping")
 
@@ -664,7 +684,7 @@ class Wrapping(Configured, PandasIndexer, AttrResolver):
     """Class that uses `ArrayWrapper` globally."""
 
     def __init__(self, wrapper: ArrayWrapper, **kwargs) -> None:
-        checks.assert_type(wrapper, ArrayWrapper)
+        checks.assert_instance_of(wrapper, ArrayWrapper)
         self._wrapper = wrapper
 
         Configured.__init__(self, wrapper=wrapper, **kwargs)
@@ -727,16 +747,40 @@ def resolve_self(self: AttrResolverT,
         return self
 
     def select_one(self: WrappingT, column: tp.Any = None, group_by: tp.GroupByLike = None, **kwargs) -> WrappingT:
-        """Select one column/group."""
+        """Select one column/group.
+
+        `column` can be a label-based position as well as an integer position (if label fails)."""
         _self = self.regroup(group_by, **kwargs)
+
+        def _check_out_dim(out: WrappingT) -> WrappingT:
+            if _self.wrapper.grouper.is_grouped():
+                if out.wrapper.grouped_ndim != 1:
+                    raise TypeError("Could not select one group: multiple groups returned")
+            else:
+                if out.wrapper.ndim != 1:
+                    raise TypeError("Could not select one column: multiple columns returned")
+            return out
+
         if column is not None:
             if _self.wrapper.grouper.is_grouped():
+                if _self.wrapper.grouped_ndim == 1:
+                    raise TypeError("This object already contains one group of data")
                 if column not in _self.wrapper.get_columns():
+                    if isinstance(column, int):
+                        if _self.wrapper.column_only_select:
+                            return _check_out_dim(_self.iloc[column])
+                        return _check_out_dim(_self.iloc[:, column])
                     raise KeyError(f"Group '{column}' not found")
             else:
+                if _self.wrapper.ndim == 1:
+                    raise TypeError("This object already contains one column of data")
                 if column not in _self.wrapper.columns:
+                    if isinstance(column, int):
+                        if _self.wrapper.column_only_select:
+                            return _check_out_dim(_self.iloc[column])
+                        return _check_out_dim(_self.iloc[:, column])
                     raise KeyError(f"Column '{column}' not found")
-            return _self[column]
+            return _check_out_dim(_self[column])
         if not _self.wrapper.grouper.is_grouped():
             if _self.wrapper.ndim == 1:
                 return _self
@@ -747,13 +791,25 @@ def select_one(self: WrappingT, column: tp.Any = None, group_by: tp.GroupByLike
 
     @staticmethod
     def select_one_from_obj(obj: tp.SeriesFrame, wrapper: ArrayWrapper, column: tp.Any = None) -> tp.MaybeSeries:
-        """Select one column/group from a pandas object."""
+        """Select one column/group from a pandas object.
+
+        `column` can be a label-based position as well as an integer position (if label fails)."""
         if column is not None:
+            if wrapper.ndim == 1:
+                raise TypeError("This object already contains one column of data")
             if wrapper.grouper.is_grouped():
                 if column not in wrapper.get_columns():
+                    if isinstance(column, int):
+                        if isinstance(obj, pd.DataFrame):
+                            return obj.iloc[:, column]
+                        return obj.iloc[column]
                     raise KeyError(f"Group '{column}' not found")
             else:
                 if column not in wrapper.columns:
+                    if isinstance(column, int):
+                        if isinstance(obj, pd.DataFrame):
+                            return obj.iloc[:, column]
+                        return obj.iloc[column]
                     raise KeyError(f"Column '{column}' not found")
             return obj[column]
         if not wrapper.grouper.is_grouped():
diff --git a/vectorbt/base/column_grouper.py b/vectorbt/base/column_grouper.py
index 8c2070a9..c6a35389 100644
--- a/vectorbt/base/column_grouper.py
+++ b/vectorbt/base/column_grouper.py
@@ -127,7 +127,7 @@ def __init__(self, columns: tp.Index, group_by: tp.GroupByLike = None, allow_ena
             allow_modify=allow_modify
         )
 
-        checks.assert_type(columns, pd.Index)
+        checks.assert_instance_of(columns, pd.Index)
         self._columns = columns
         if group_by is None or group_by is False:
             self._group_by = None
diff --git a/vectorbt/base/index_fns.py b/vectorbt/base/index_fns.py
index 69229d97..5abbf043 100644
--- a/vectorbt/base/index_fns.py
+++ b/vectorbt/base/index_fns.py
@@ -23,7 +23,7 @@ def to_any_index(index_like: tp.IndexLike) -> tp.Index:
 
 def get_index(arg: tp.SeriesFrame, axis: int) -> tp.Index:
     """Get index of `arg` by `axis`."""
-    checks.assert_type(arg, (pd.Series, pd.DataFrame))
+    checks.assert_instance_of(arg, (pd.Series, pd.DataFrame))
     checks.assert_in(axis, (0, 1))
 
     if axis == 0:
@@ -176,7 +176,7 @@ def rename_levels(index: tp.Index, name_dict: tp.Dict[str, tp.Any], strict: bool
 
 def select_levels(index: tp.Index, level_names: tp.MaybeLevelSequence) -> tp.Index:
     """Build a new index by selecting one or multiple `level_names` from `index`."""
-    checks.assert_type(index, pd.MultiIndex)
+    checks.assert_instance_of(index, pd.MultiIndex)
 
     if isinstance(level_names, (int, str)):
         return index.get_level_values(level_names)
@@ -335,7 +335,7 @@ def pick_levels(index: tp.Index,
         required_levels = []
     if optional_levels is None:
         optional_levels = []
-    checks.assert_type(index, pd.MultiIndex)
+    checks.assert_instance_of(index, pd.MultiIndex)
 
     n_opt_set = len(list(filter(lambda x: x is not None, optional_levels)))
     n_req_set = len(list(filter(lambda x: x is not None, required_levels)))
@@ -352,7 +352,7 @@ def pick_levels(index: tp.Index,
     for level in optional_levels:
         level_pos = None
         if level is not None:
-            checks.assert_type(level, (int, str))
+            checks.assert_instance_of(level, (int, str))
             if isinstance(level, str):
                 level_pos = index.names.index(level)
             else:
@@ -367,7 +367,7 @@ def pick_levels(index: tp.Index,
     for level in required_levels:
         level_pos = None
         if level is not None:
-            checks.assert_type(level, (int, str))
+            checks.assert_instance_of(level, (int, str))
             if isinstance(level, str):
                 level_pos = index.names.index(level)
             else:
diff --git a/vectorbt/base/indexing.py b/vectorbt/base/indexing.py
index 115ba747..52fbbbed 100644
--- a/vectorbt/base/indexing.py
+++ b/vectorbt/base/indexing.py
@@ -149,7 +149,7 @@ class ParamLoc(LocBase):
     Uses `mapper` to establish link between columns and parameter values."""
 
     def __init__(self, mapper: tp.Series, indexing_func: tp.Callable, level_name: tp.Level = None, **kwargs) -> None:
-        checks.assert_type(mapper, pd.Series)
+        checks.assert_instance_of(mapper, pd.Series)
 
         if mapper.dtype == 'O':
             # If params are objects, we must cast them to string first
@@ -210,8 +210,8 @@ def pd_indexing_func(obj: tp.SeriesFrame) -> tp.MaybeSeriesFrame:
 def indexing_on_mapper(mapper: tp.Series, ref_obj: tp.SeriesFrame,
                        pd_indexing_func: tp.Callable) -> tp.Optional[tp.Series]:
     """Broadcast `mapper` Series to `ref_obj` and perform pandas indexing using `pd_indexing_func`."""
-    checks.assert_type(mapper, pd.Series)
-    checks.assert_type(ref_obj, (pd.Series, pd.DataFrame))
+    checks.assert_instance_of(mapper, pd.Series)
+    checks.assert_instance_of(ref_obj, (pd.Series, pd.DataFrame))
 
     df_range_mapper = reshape_fns.broadcast_to(np.arange(len(mapper.index)), ref_obj)
     loced_range_mapper = pd_indexing_func(df_range_mapper)
diff --git a/vectorbt/base/reshape_fns.py b/vectorbt/base/reshape_fns.py
index ca6912ab..3f10f2df 100644
--- a/vectorbt/base/reshape_fns.py
+++ b/vectorbt/base/reshape_fns.py
@@ -522,7 +522,7 @@ def broadcast(*args: tp.ArrayLike,
     if to_shape is not None:
         if isinstance(to_shape, int):
             to_shape = (to_shape,)
-        checks.assert_type(to_shape, tuple)
+        checks.assert_instance_of(to_shape, tuple)
         if len(to_shape) > 1:
             is_2d = True
 
@@ -725,7 +725,7 @@ def get_multiindex_series(arg: tp.SeriesFrame) -> tp.Series:
     """Get Series with a multi-index.
 
     If DataFrame has been passed, should at maximum have one row or column."""
-    checks.assert_type(arg, (pd.Series, pd.DataFrame))
+    checks.assert_instance_of(arg, (pd.Series, pd.DataFrame))
     if checks.is_frame(arg):
         if arg.shape[0] == 1:
             arg = arg.iloc[0, :]
@@ -733,7 +733,7 @@ def get_multiindex_series(arg: tp.SeriesFrame) -> tp.Series:
             arg = arg.iloc[:, 0]
         else:
             raise ValueError("Supported are either Series or DataFrame with one column/row")
-    checks.assert_type(arg.index, pd.MultiIndex)
+    checks.assert_instance_of(arg.index, pd.MultiIndex)
     return arg
 
 
@@ -819,11 +819,11 @@ def make_symmetric(arg: tp.SeriesFrame, sort: bool = True) -> tp.Frame:
     d  2.0  4.0  NaN  NaN
     ```
     """
-    checks.assert_type(arg, (pd.Series, pd.DataFrame))
+    checks.assert_instance_of(arg, (pd.Series, pd.DataFrame))
     df: tp.Frame = to_2d(arg)
     if isinstance(df.index, pd.MultiIndex) or isinstance(df.columns, pd.MultiIndex):
-        checks.assert_type(df.index, pd.MultiIndex)
-        checks.assert_type(df.columns, pd.MultiIndex)
+        checks.assert_instance_of(df.index, pd.MultiIndex)
+        checks.assert_instance_of(df.columns, pd.MultiIndex)
         checks.assert_array_equal(df.index.nlevels, df.columns.nlevels)
         names1, names2 = tuple(df.index.names), tuple(df.columns.names)
     else:
diff --git a/vectorbt/data/base.py b/vectorbt/data/base.py
index 8bbbcbca..352ca640 100644
--- a/vectorbt/data/base.py
+++ b/vectorbt/data/base.py
@@ -254,7 +254,7 @@ def __init__(self,
             **kwargs
         )
 
-        checks.assert_type(data, dict)
+        checks.assert_instance_of(data, dict)
         for k, v in data.items():
             checks.assert_meta_equal(v, data[list(data.keys())[0]])
         self._data = data
diff --git a/vectorbt/generic/__init__.py b/vectorbt/generic/__init__.py
index be5b2482..88be331b 100644
--- a/vectorbt/generic/__init__.py
+++ b/vectorbt/generic/__init__.py
@@ -3,10 +3,12 @@
 In contrast to the `vectorbt.base` sub-package, focuses on the data itself."""
 
 from vectorbt.generic.enums import *
+from vectorbt.generic.ranges import Ranges
 from vectorbt.generic.drawdowns import Drawdowns
 from vectorbt.generic.splitters import RangeSplitter, RollingSplitter, ExpandingSplitter
 
 __all__ = [
+    'Ranges',
     'Drawdowns',
     'RangeSplitter',
     'RollingSplitter',
diff --git a/vectorbt/generic/accessors.py b/vectorbt/generic/accessors.py
index dcb3bf93..b2007d7c 100644
--- a/vectorbt/generic/accessors.py
+++ b/vectorbt/generic/accessors.py
@@ -212,10 +212,11 @@
 from vectorbt.base.accessors import BaseAccessor, BaseDFAccessor, BaseSRAccessor
 from vectorbt.base.array_wrapper import ArrayWrapper, Wrapping
 from vectorbt.generic import plotting, nb
+from vectorbt.generic.ranges import Ranges
 from vectorbt.generic.drawdowns import Drawdowns
 from vectorbt.generic.splitters import SplitterT, RangeSplitter, RollingSplitter, ExpandingSplitter
 from vectorbt.generic.stats_builder import StatsBuilderMixin
-from vectorbt.generic.decorators import add_nb_methods, add_transform_methods
+from vectorbt.generic.decorators import attach_nb_methods, attach_transform_methods
 from vectorbt.records.mapped_array import MappedArray
 
 try:  # pragma: no cover
@@ -276,9 +277,8 @@ def fit_transform(self, *args, **kwargs) -> tp.Array2d:
         'expanding_mean': dict(func=nb.expanding_mean_nb, path='vectorbt.generic.nb.expanding_mean_nb'),
         'product': dict(func=nb.nanprod_nb, is_reducing=True, path='vectorbt.generic.nb.nanprod_nb')
     },
-    as_attrs=False,
     readonly=True,
-    copy_kwargs=dict(copy_mode='deep')
+    as_attrs=False
 )
 """_"""
 
@@ -324,9 +324,8 @@ def fit_transform(self, *args, **kwargs) -> tp.Array2d:
             docstring="See `sklearn.preprocessing.PowerTransformer`."
         )
     },
-    as_attrs=False,
     readonly=True,
-    copy_kwargs=dict(copy_mode='deep')
+    as_attrs=False
 )
 """_"""
 
@@ -338,21 +337,26 @@ def fit_transform(self, *args, **kwargs) -> tp.Array2d:
 """
 
 
-@add_nb_methods(nb_config)
-@add_transform_methods(transform_config)
+@attach_nb_methods(nb_config)
+@attach_transform_methods(transform_config)
 class GenericAccessor(BaseAccessor, StatsBuilderMixin):
     """Accessor on top of data of any type. For both, Series and DataFrames.
 
     Accessible through `pd.Series.vbt` and `pd.DataFrame.vbt`."""
 
     def __init__(self, obj: tp.SeriesFrame, mapping: tp.Optional[tp.MappingLike] = None, **kwargs) -> None:
+        BaseAccessor.__init__(self, obj, mapping=mapping, **kwargs)
+        StatsBuilderMixin.__init__(self)
+
         if mapping is not None:
+            if isinstance(mapping, str):
+                if mapping.lower() == 'index':
+                    mapping = self.wrapper.index
+                elif mapping.lower() == 'columns':
+                    mapping = self.wrapper.columns
             mapping = to_mapping(mapping)
         self._mapping = mapping
 
-        BaseAccessor.__init__(self, obj, mapping=mapping, **kwargs)
-        StatsBuilderMixin.__init__(self)
-
     @property
     def sr_accessor_cls(self):
         """Accessor class for `pd.Series`."""
@@ -954,6 +958,12 @@ def value_counts(self,
         Mapping will be applied using `vectorbt.utils.mapping.apply_mapping` with `**kwargs`."""
         if mapping is None:
             mapping = self.mapping
+        if isinstance(mapping, str):
+            if mapping.lower() == 'index':
+                mapping = self.wrapper.index
+            elif mapping.lower() == 'columns':
+                mapping = self.wrapper.columns
+            mapping = to_mapping(mapping)
         codes, uniques = pd.factorize(self.obj.values.flatten(), sort=False, na_sentinel=None)
         codes = codes.reshape(self.wrapper.shape_2d)
         group_lens = self.wrapper.grouper.get_group_lens(group_by=group_by)
@@ -1144,18 +1154,29 @@ def drawdown(self, wrap_kwargs: tp.KwargsLike = None) -> tp.SeriesFrame:
         out = self.to_2d_array() / nb.expanding_max_nb(self.to_2d_array()) - 1
         return self.wrapper.wrap(out, group_by=False, **merge_dicts({}, wrap_kwargs))
 
+    @property
+    def ranges(self) -> Ranges:
+        """`GenericAccessor.get_ranges` with default arguments."""
+        return self.get_ranges()
+
+    def get_ranges(self, wrapper_kwargs: tp.KwargsLike = None, **kwargs) -> Ranges:
+        """Generate range records.
+
+        See `vectorbt.generic.ranges.Ranges`."""
+        wrapper_kwargs = merge_dicts(self.wrapper.config, wrapper_kwargs)
+        return Ranges.from_ts(self.obj, wrapper_kwargs=wrapper_kwargs, **kwargs)
+
     @property
     def drawdowns(self) -> Drawdowns:
         """`GenericAccessor.get_drawdowns` with default arguments."""
         return self.get_drawdowns()
 
-    def get_drawdowns(self, group_by: tp.GroupByLike = None, **kwargs) -> Drawdowns:
+    def get_drawdowns(self, wrapper_kwargs: tp.KwargsLike = None, **kwargs) -> Drawdowns:
         """Generate drawdown records.
 
         See `vectorbt.generic.drawdowns.Drawdowns`."""
-        if group_by is None:
-            group_by = self.wrapper.grouper.group_by
-        return Drawdowns.from_ts(self.obj, freq=self.wrapper.freq, group_by=group_by, **kwargs)
+        wrapper_kwargs = merge_dicts(self.wrapper.config, wrapper_kwargs)
+        return Drawdowns.from_ts(self.obj, wrapper_kwargs=wrapper_kwargs, **kwargs)
 
     def to_mapped(self,
                   dropna: bool = True,
@@ -1287,7 +1308,7 @@ def split(self, splitter: SplitterT, stack_kwargs: tp.KwargsLike = None, keys: t
         >>> sr.vbt.split(splitter, plot=True, trace_names=['train', 'test'])
         ```
 
-        ![](/vectorbt/docs/img/split_plot.svg)
+        ![](/docs/img/split_plot.svg)
         """
         total_range_sr = pd.Series(np.arange(len(self.wrapper.index)), index=self.wrapper.index)
         set_ranges = list(splitter.split(total_range_sr, **kwargs))
@@ -1435,7 +1456,7 @@ def range_split(self, **kwargs) -> SplitOutputT:
          ... )
         ```
 
-        ![](/vectorbt/docs/img/range_split_plot.svg)
+        ![](/docs/img/range_split_plot.svg)
         """
         return self.split(RangeSplitter(), **kwargs)
 
@@ -1464,7 +1485,7 @@ def rolling_split(self, **kwargs) -> SplitOutputT:
         ...     plot=True, trace_names=['train', 'valid', 'test'])
         ```
 
-        ![](/vectorbt/docs/img/rolling_split_plot.svg)
+        ![](/docs/img/rolling_split_plot.svg)
         """
         return self.split(RollingSplitter(), **kwargs)
 
@@ -1498,7 +1519,7 @@ def expanding_split(self, **kwargs) -> SplitOutputT:
         ...     plot=True, trace_names=['train', 'valid', 'test'])
         ```
 
-        ![](/vectorbt/docs/img/expanding_split_plot.svg)
+        ![](/docs/img/expanding_split_plot.svg)
         """
         return self.split(ExpandingSplitter(), **kwargs)
 
@@ -1517,7 +1538,7 @@ def plot(self,
         >>> df.vbt.plot()
         ```
 
-        ![](/vectorbt/docs/img/df_plot.svg)
+        ![](/docs/img/df_plot.svg)
         """
         if x_labels is None:
             x_labels = self.wrapper.index
@@ -1543,7 +1564,7 @@ def lineplot(self, **kwargs) -> tp.Union[tp.BaseFigure, plotting.Scatter]:  # pr
         >>> df.vbt.lineplot()
         ```
 
-        ![](/vectorbt/docs/img/df_lineplot.svg)
+        ![](/docs/img/df_lineplot.svg)
         """
         return self.plot(**merge_dicts(dict(trace_kwargs=dict(mode='lines')), kwargs))
 
@@ -1556,7 +1577,7 @@ def scatterplot(self, **kwargs) -> tp.Union[tp.BaseFigure, plotting.Scatter]:  #
         >>> df.vbt.scatterplot()
         ```
 
-        ![](/vectorbt/docs/img/df_scatterplot.svg)
+        ![](/docs/img/df_scatterplot.svg)
         """
         return self.plot(**merge_dicts(dict(trace_kwargs=dict(mode='markers')), kwargs))
 
@@ -1573,7 +1594,7 @@ def barplot(self,
         >>> df.vbt.barplot()
         ```
 
-        ![](/vectorbt/docs/img/df_barplot.svg)
+        ![](/docs/img/df_barplot.svg)
         """
         if x_labels is None:
             x_labels = self.wrapper.index
@@ -1603,7 +1624,7 @@ def histplot(self,
         >>> df.vbt.histplot()
         ```
 
-        ![](/vectorbt/docs/img/df_histplot.svg)
+        ![](/docs/img/df_histplot.svg)
         """
         if self.wrapper.grouper.is_grouped(group_by=group_by):
             return self.flatten_grouped(group_by=group_by).vbt.histplot(trace_names=trace_names, **kwargs)
@@ -1633,7 +1654,7 @@ def boxplot(self,
         >>> df.vbt.boxplot()
         ```
 
-        ![](/vectorbt/docs/img/df_boxplot.svg)
+        ![](/docs/img/df_boxplot.svg)
         """
         if self.wrapper.grouper.is_grouped(group_by=group_by):
             return self.flatten_grouped(group_by=group_by).vbt.boxplot(trace_names=trace_names, **kwargs)
@@ -1713,7 +1734,7 @@ def plot_against(self,
         >>> df['a'].vbt.plot_against(df['b'])
         ```
 
-        ![](/vectorbt/docs/img/sr_plot_against.svg)
+        ![](/docs/img/sr_plot_against.svg)
         """
         if trace_kwargs is None:
             trace_kwargs = {}
@@ -1726,7 +1747,7 @@ def plot_against(self,
         if hidden_trace_kwargs is None:
             hidden_trace_kwargs = {}
         obj, other = reshape_fns.broadcast(self.obj, other, columns_from='keep')
-        checks.assert_type(other, pd.Series)
+        checks.assert_instance_of(other, pd.Series)
         if fig is None:
             fig = make_figure()
         fig.update_layout(**layout_kwargs)
@@ -1844,7 +1865,7 @@ def overlay_with_heatmap(self,
         >>> df['a'].vbt.overlay_with_heatmap(df['b'])
         ```
 
-        ![](/vectorbt/docs/img/sr_overlay_with_heatmap.svg)
+        ![](/docs/img/sr_overlay_with_heatmap.svg)
         """
         from vectorbt._settings import settings
         plotting_cfg = settings['plotting']
@@ -1857,7 +1878,7 @@ def overlay_with_heatmap(self,
             add_trace_kwargs = {}
 
         obj, other = reshape_fns.broadcast(self.obj, other, columns_from='keep')
-        checks.assert_type(other, pd.Series)
+        checks.assert_instance_of(other, pd.Series)
         if fig is None:
             fig = make_subplots(specs=[[{"secondary_y": True}]])
             if 'width' in plotting_cfg['layout']:
@@ -1913,7 +1934,7 @@ def heatmap(self,
         >>> sr.vbt.heatmap()
         ```
 
-        ![](/vectorbt/docs/img/sr_heatmap.svg)
+        ![](/docs/img/sr_heatmap.svg)
 
         Using one level as a slider:
 
@@ -1939,7 +1960,7 @@ def heatmap(self,
         >>> sr.vbt.heatmap(slider_level=0)
         ```
 
-        ![](/vectorbt/docs/img/sr_heatmap_slider.gif)
+        ![](/docs/img/sr_heatmap_slider.gif)
         """
         if not isinstance(self.wrapper.index, pd.MultiIndex):
             return self.obj.to_frame().vbt.heatmap(
@@ -2075,7 +2096,7 @@ def volume(self,
         >>> sr.vbt.volume().show()
         ```
 
-        ![](/vectorbt/docs/img/sr_volume.svg)
+        ![](/docs/img/sr_volume.svg)
         """
         (x_level, y_level, z_level), (slider_level,) = index_fns.pick_levels(
             self.wrapper.index,
@@ -2210,7 +2231,7 @@ def qqplot(self,
         >>> pd.Series(np.random.standard_normal(100)).vbt.qqplot()
         ```
 
-        ![](/vectorbt/docs/img/sr_qqplot.svg)
+        ![](/docs/img/sr_qqplot.svg)
         """
         qq = stats.probplot(self.obj, sparams=sparams, dist=dist)
         fig = pd.Series(qq[0][1], index=qq[0][0]).vbt.scatterplot(fig=fig, **kwargs)
@@ -2355,7 +2376,7 @@ def heatmap(self,
         >>> df.vbt.heatmap()
         ```
 
-        ![](/vectorbt/docs/img/df_heatmap.svg)
+        ![](/docs/img/df_heatmap.svg)
         """
         if x_labels is None:
             x_labels = self.wrapper.columns
diff --git a/vectorbt/generic/decorators.py b/vectorbt/generic/decorators.py
index 032022f2..e916c5fc 100644
--- a/vectorbt/generic/decorators.py
+++ b/vectorbt/generic/decorators.py
@@ -9,7 +9,7 @@
 WrapperFuncT = tp.Callable[[tp.Type[tp.T]], tp.Type[tp.T]]
 
 
-def add_nb_methods(config: Config) -> WrapperFuncT:
+def attach_nb_methods(config: Config) -> WrapperFuncT:
     """Class decorator to add Numba methods.
 
     `config` should contain target method names (keys) and dictionaries (values) with the following keys:
@@ -27,7 +27,7 @@ def add_nb_methods(config: Config) -> WrapperFuncT:
     def wrapper(cls: tp.Type[tp.T]) -> tp.Type[tp.T]:
         from vectorbt.base.array_wrapper import Wrapping
 
-        checks.assert_subclass(cls, Wrapping)
+        checks.assert_subclass_of(cls, Wrapping)
 
         for target_name, settings in config.items():
             func = settings['func']
@@ -72,7 +72,7 @@ def new_method(self,
     return wrapper
 
 
-def add_transform_methods(config: Config) -> WrapperFuncT:
+def attach_transform_methods(config: Config) -> WrapperFuncT:
     """Class decorator to add transformation methods.
 
     `config` should contain target method names (keys) and dictionaries (values) with the following keys:
@@ -87,7 +87,7 @@ def add_transform_methods(config: Config) -> WrapperFuncT:
     def wrapper(cls: tp.Type[tp.T]) -> tp.Type[tp.T]:
         from vectorbt.generic.accessors import TransformerT
 
-        checks.assert_subclass(cls, "GenericAccessor")
+        checks.assert_subclass_of(cls, "GenericAccessor")
 
         for target_name, settings in config.items():
             transformer = settings['transformer']
diff --git a/vectorbt/generic/drawdowns.py b/vectorbt/generic/drawdowns.py
index 98d16d16..a29aa930 100644
--- a/vectorbt/generic/drawdowns.py
+++ b/vectorbt/generic/drawdowns.py
@@ -1,18 +1,54 @@
 """Base class for working with drawdown records.
 
-Class `Drawdowns` accepts drawdown records and the corresponding time series
-to analyze the periods of drawdown. Using `Drawdowns.from_ts`, you can generate
-drawdown records for any time series and analyze them right away.
+Drawdown records capture information on drawdowns. Since drawdowns are ranges,
+they subclass `vectorbt.generic.ranges.Ranges`.
 
-Moreover, all time series accessors have a method `drawdowns`:
+!!! warning
+    `Drawdowns` return both recovered AND active drawdowns, which may skew your performance results.
+    To only consider recovered drawdowns, you should explicitly query `recovered` attribute.
+
+Using `Drawdowns.from_ts`, you can generate drawdown records for any time series and analyze them right away.
 
 ```python-repl
->>> import pandas as pd
 >>> import vectorbt as vbt
+>>> import numpy as np
+>>> import pandas as pd
+
+>>> start = '2019-10-01 UTC'  # crypto is in UTC
+>>> end = '2020-01-01 UTC'
+>>> price = vbt.YFData.download('BTC-USD', start=start, end=end).get('Close')
+>>> price = price.rename(None)
+
+>>> drawdowns = vbt.Drawdowns.from_ts(price, wrapper_kwargs=dict(freq='d'))
+
+>>> drawdowns.records_readable
+   Drawdown Id  Column                 Peak Date                Start Date  \
+0            0       0 2019-10-02 00:00:00+00:00 2019-10-03 00:00:00+00:00
+1            1       0 2019-10-09 00:00:00+00:00 2019-10-10 00:00:00+00:00
+2            2       0 2019-10-27 00:00:00+00:00 2019-10-28 00:00:00+00:00
+
+                Valley Date                  End Date   Peak Value  \
+0 2019-10-06 00:00:00+00:00 2019-10-09 00:00:00+00:00  8393.041992
+1 2019-10-24 00:00:00+00:00 2019-10-25 00:00:00+00:00  8595.740234
+2 2019-12-17 00:00:00+00:00 2020-01-01 00:00:00+00:00  9551.714844
+
+   Valley Value    End Value     Status
+0   7988.155762  8393.041992  Recovered
+1   7493.488770  8595.740234  Recovered
+2   6640.515137  7200.174316     Active
+
+>>> drawdowns.duration.max(wrap_kwargs=dict(to_timedelta=True))
+Timedelta('66 days 00:00:00')
+```
+
+## From accessors
 
->>> # vectorbt.generic.accessors.GenericAccessor.drawdowns.active_drawdown
->>> pd.Series([5, 4, 3, 4]).vbt.drawdowns.active_drawdown()
--0.2
+Moreover, all generic accessors have a property `drawdowns` and a method `get_drawdowns`:
+
+```python-repl
+>>> # vectorbt.generic.accessors.GenericAccessor.drawdowns.coverage
+>>> price.vbt.drawdowns.coverage()
+0.9354838709677419
 ```
 
 ## Stats
@@ -26,10 +62,13 @@
 ...     'b': [2, 3, 1, 2, 1]
 ... })
 
->>> df['a'].vbt(freq='d').drawdowns.stats()
+>>> drawdowns = df.vbt(freq='d').drawdowns
+
+>>> drawdowns['a'].stats()
 Start                                        0
 End                                          4
 Period                         5 days 00:00:00
+Coverage [%]                              40.0
 Total Records                                2
 Total Recovered Drawdowns                    1
 Total Active Drawdowns                       1
@@ -40,13 +79,13 @@
 Active Recovery Duration       0 days 00:00:00
 Max Drawdown [%]                          50.0
 Avg Drawdown [%]                          50.0
-Max Drawdown Duration          2 days 00:00:00
-Avg Drawdown Duration          2 days 00:00:00
-Max Recovery Return [%]                  200.0
-Avg Recovery Return [%]                  200.0
+Max Drawdown Duration          1 days 00:00:00
+Avg Drawdown Duration          1 days 00:00:00
+Max Recovery Return [%]                  100.0
+Avg Recovery Return [%]                  100.0
 Max Recovery Duration          1 days 00:00:00
 Avg Recovery Duration          1 days 00:00:00
-Avg Recovery Duration Ratio                0.5
+Avg Recovery Duration Ratio                1.0
 Name: a, dtype: object
 ```
 
@@ -54,10 +93,11 @@
 not include active drawdowns. To change that, pass `incl_active=True`:
 
 ```python-repl
->>> df['a'].vbt(freq='d').drawdowns.stats(settings=dict(incl_active=True))
+>>> drawdowns['a'].stats(settings=dict(incl_active=True))
 Start                                        0
 End                                          4
 Period                         5 days 00:00:00
+Coverage [%]                              40.0
 Total Records                                2
 Total Recovered Drawdowns                    1
 Total Active Drawdowns                       1
@@ -68,35 +108,42 @@
 Active Recovery Duration       0 days 00:00:00
 Max Drawdown [%]                          50.0
 Avg Drawdown [%]                     41.666667
-Max Drawdown Duration          2 days 00:00:00
-Avg Drawdown Duration          1 days 12:00:00
-Max Recovery Return [%]                  200.0
-Avg Recovery Return [%]                  200.0
+Max Drawdown Duration          1 days 00:00:00
+Avg Drawdown Duration          1 days 00:00:00
+Max Recovery Return [%]                  100.0
+Avg Recovery Return [%]                  100.0
 Max Recovery Duration          1 days 00:00:00
 Avg Recovery Duration          1 days 00:00:00
-Avg Recovery Duration Ratio                0.5
+Avg Recovery Duration Ratio                1.0
 Name: a, dtype: object
 ```
 
 `Drawdowns.stats` also supports (re-)grouping:
 
 ```python-repl
->>> df.vbt(freq='d', group_by=True).drawdowns.stats()
+>>> drawdowns['a'].stats(group_by=True)
+UserWarning: Metric 'active_dd' does not support grouped data
+UserWarning: Metric 'active_duration' does not support grouped data
+UserWarning: Metric 'active_recovery' does not support grouped data
+UserWarning: Metric 'active_recovery_return' does not support grouped data
+UserWarning: Metric 'active_recovery_duration' does not support grouped data
+
 Start                                        0
 End                                          4
 Period                         5 days 00:00:00
-Total Records                                3
+Coverage [%]                              40.0
+Total Records                                2
 Total Recovered Drawdowns                    1
-Total Active Drawdowns                       2
+Total Active Drawdowns                       1
 Max Drawdown [%]                          50.0
 Avg Drawdown [%]                          50.0
-Max Drawdown Duration          2 days 00:00:00
-Avg Drawdown Duration          2 days 00:00:00
-Max Recovery Return [%]                  200.0
-Avg Recovery Return [%]                  200.0
+Max Drawdown Duration          1 days 00:00:00
+Avg Drawdown Duration          1 days 00:00:00
+Max Recovery Return [%]                  100.0
+Avg Recovery Return [%]                  100.0
 Max Recovery Duration          1 days 00:00:00
 Avg Recovery Duration          1 days 00:00:00
-Avg Recovery Duration Ratio                0.5
+Avg Recovery Duration Ratio                1.0
 Name: group, dtype: object
 ```
 """
@@ -109,105 +156,114 @@
 from vectorbt.utils.decorators import cached_property, cached_method
 from vectorbt.utils.config import merge_dicts, Config
 from vectorbt.utils.colors import adjust_lightness
-from vectorbt.utils.datetime import DatetimeIndexes
-from vectorbt.utils.enum import map_enum_values
 from vectorbt.utils.figure import make_figure, get_domain
 from vectorbt.utils.template import RepEval
-from vectorbt.base.reshape_fns import to_1d_array, to_2d_array, broadcast_to, to_pd_array
+from vectorbt.base.reshape_fns import to_2d_array, to_pd_array
 from vectorbt.base.array_wrapper import ArrayWrapper
 from vectorbt.generic import nb
 from vectorbt.generic.enums import DrawdownStatus, drawdown_dt
 from vectorbt.generic.stats_builder import StatsBuilderMixin
-from vectorbt.records.base import Records
+from vectorbt.generic.ranges import Ranges
 from vectorbt.records.mapped_array import MappedArray
-from vectorbt.records.decorators import add_mapped_fields
+from vectorbt.records.decorators import override_field_config, attach_fields
 
 __pdoc__ = {}
 
-drawdowns_mf_config = Config(
+dd_field_config = Config(
     dict(
-        status=dict(defaults=dict(mapping=DrawdownStatus))
+        dtype=drawdown_dt,
+        settings=dict(
+            id=dict(
+                title='Drawdown Id'
+            ),
+            peak_idx=dict(
+                title='Peak Date',
+                mapping='index'
+            ),
+            valley_idx=dict(
+                title='Valley Date',
+                mapping='index'
+            ),
+            peak_val=dict(
+                title='Peak Value',
+            ),
+            valley_val=dict(
+                title='Valley Value',
+            ),
+            end_val=dict(
+                title='End Value',
+            ),
+            status=dict(
+                mapping=DrawdownStatus
+            )
+        )
     ),
-    as_attrs=False,
     readonly=True,
-    copy_kwargs=dict(copy_mode='deep')
+    as_attrs=False
 )
 """_"""
 
-__pdoc__['drawdowns_mf_config'] = f"""Config of `vectorbt.generic.enums.drawdown_dt` 
-mapped fields to be overridden in `Drawdowns`.
+__pdoc__['dd_field_config'] = f"""Field config for `Drawdowns`.
 
 ```json
-{drawdowns_mf_config.to_doc()}
+{dd_field_config.to_doc()}
 ```
 """
 
-DrawdownsT = tp.TypeVar("DrawdownsT", bound="Drawdowns")
-
-
-@add_mapped_fields(drawdown_dt, drawdowns_mf_config)
-class Drawdowns(Records):
-    """Extends `Records` for working with drawdown records.
-
-    Requires `records_arr` to have all fields defined in `vectorbt.generic.enums.drawdown_dt`.
-
-    ## Example
+dd_attach_field_config = Config(
+    dict(
+        status=dict(
+            attach_filters=True
+        )
+    ),
+    readonly=True,
+    as_attrs=False
+)
+"""_"""
 
-    ```python-repl
-    >>> import vectorbt as vbt
-    >>> import numpy as np
-    >>> import pandas as pd
-    >>> from datetime import datetime
+__pdoc__['dd_attach_field_config'] = f"""Config of fields to be attached to `Drawdowns`.
 
-    >>> start = '2019-01-01 UTC'  # crypto is in UTC
-    >>> end = '2020-01-01 UTC'
-    >>> price = vbt.YFData.download('BTC-USD', start=start, end=end).get('Close')
-    >>> drawdowns = vbt.Drawdowns.from_ts(price, freq='1 days')
+```json
+{dd_attach_field_config.to_doc()}
+```
+"""
 
-    >>> drawdowns.records.head()
-       id  col  start_idx  valley_idx  end_idx  status
-    0   0    0          1           2        5       1
-    1   1    0          5          37       53       1
-    2   2    0         53          62       90       1
-    3   3    0         92          93       94       1
-    4   4    0         97          98       99       1
+DrawdownsT = tp.TypeVar("DrawdownsT", bound="Drawdowns")
 
-    >>> drawdowns.drawdown
-    <vectorbt.records.base.MappedArray at 0x7fafa6a11160>
 
-    >>> drawdowns.drawdown.min()
-    -0.48982813000684766
+@attach_fields(dd_attach_field_config)
+@override_field_config(dd_field_config)
+class Drawdowns(Ranges):
+    """Extends `vectorbt.generic.ranges.Ranges` for working with drawdown records.
 
-    >>> drawdowns.drawdown.histplot(trace_kwargs=dict(nbinsx=50))
-    ```
+    Requires `records_arr` to have all fields defined in `vectorbt.generic.enums.drawdown_dt`."""
 
-    ![](/vectorbt/docs/img/drawdowns_drawdown_histplot.svg)
-    """
+    @property
+    def field_config(self) -> Config:
+        return self._field_config
 
     def __init__(self,
                  wrapper: ArrayWrapper,
                  records_arr: tp.RecordArray,
-                 ts: tp.ArrayLike,
-                 idx_field: str = 'end_idx',
+                 ts: tp.Optional[tp.ArrayLike] = None,
                  **kwargs) -> None:
-        Records.__init__(
+        Ranges.__init__(
             self,
             wrapper,
             records_arr,
-            idx_field=idx_field,
             ts=ts,
             **kwargs
         )
-        self._ts = broadcast_to(ts, wrapper.dummy(group_by=False))
-
-        if not all(field in records_arr.dtype.names for field in drawdown_dt.names):
-            raise TypeError("Records array must match drawdown_dt")
+        self._ts = ts
 
     def indexing_func(self: DrawdownsT, pd_indexing_func: tp.PandasIndexingFunc, **kwargs) -> DrawdownsT:
         """Perform indexing on `Drawdowns`."""
         new_wrapper, new_records_arr, _, col_idxs = \
-            Records.indexing_func_meta(self, pd_indexing_func, **kwargs)
-        new_ts = new_wrapper.wrap(self.ts.values[:, col_idxs], group_by=False)
+            Ranges.indexing_func_meta(self, pd_indexing_func, **kwargs)
+        if self.ts is not None:
+            new_ts = new_wrapper.wrap(self.ts.values[:, col_idxs], group_by=False)
+        else:
+            new_ts = None
         return self.copy(
             wrapper=new_wrapper,
             records_arr=new_records_arr,
@@ -215,175 +271,142 @@ def indexing_func(self: DrawdownsT, pd_indexing_func: tp.PandasIndexingFunc, **k
         )
 
     @classmethod
-    def from_ts(cls: tp.Type[DrawdownsT], ts: tp.ArrayLike, idx_field: str = 'end_idx', **kwargs) -> DrawdownsT:
+    def from_ts(cls: tp.Type[DrawdownsT], ts: tp.ArrayLike,
+                wrapper_kwargs: tp.KwargsLike = None, **kwargs) -> DrawdownsT:
         """Build `Drawdowns` from time series `ts`.
 
-        `**kwargs` such as `freq` will be passed to `Drawdowns.__init__`."""
-        pd_ts = to_pd_array(ts)
-        records_arr = nb.find_drawdowns_nb(to_2d_array(pd_ts))
-        wrapper = ArrayWrapper.from_obj(pd_ts, **kwargs)
-        return cls(wrapper, records_arr, pd_ts, idx_field=idx_field)
+        `**kwargs` will be passed to `Drawdowns.__init__`."""
+        ts_pd = to_pd_array(ts)
+        records_arr = nb.find_drawdowns_nb(to_2d_array(ts_pd))
+        wrapper = ArrayWrapper.from_obj(ts_pd, **merge_dicts({}, wrapper_kwargs))
+        return cls(wrapper, records_arr, ts=ts_pd, **kwargs)
 
     @property
-    def ts(self) -> tp.SeriesFrame:
-        """Original time series that records are built from."""
+    def ts(self) -> tp.Optional[tp.SeriesFrame]:
+        """Original time series that records are built from (optional)."""
         return self._ts
 
-    @property  # no need for cached
-    def records_readable(self) -> tp.Frame:
-        """Records in readable format."""
-        df = self.records.copy()
-        df.columns = [
-            'Drawdown Id',
-            'Column',
-            'Start Date',
-            'Valley Date',
-            'End Date',
-            'Status'
-        ]
-        df['Column'] = df['Column'].map(lambda x: self.wrapper.columns[x])
-        df['Start Date'] = df['Start Date'].map(lambda x: self.wrapper.index[x])
-        df['Valley Date'] = df['Valley Date'].map(lambda x: self.wrapper.index[x])
-        df['End Date'] = df['End Date'].map(lambda x: self.wrapper.index[x])
-        df['Status'] = map_enum_values(df['Status'], DrawdownStatus)
-        return df
-
-    @cached_property
-    def start_value(self) -> MappedArray:
-        """Start value of each drawdown."""
-        return self.map(nb.dd_start_value_map_nb, to_2d_array(self.ts))
-
-    @cached_property
-    def valley_value(self) -> MappedArray:
-        """Valley value of each drawdown."""
-        return self.map(nb.dd_valley_value_map_nb, to_2d_array(self.ts))
-
-    @cached_property
-    def end_value(self) -> MappedArray:
-        """End value of each drawdown."""
-        return self.map(nb.dd_end_value_map_nb, to_2d_array(self.ts))
+    # ############# Drawdown ############# #
 
     @cached_property
     def drawdown(self) -> MappedArray:
-        """Drawdown value (in percentage)."""
-        return self.map(nb.dd_drawdown_map_nb, to_2d_array(self.ts))
+        """See `vectorbt.generic.nb.dd_drawdown_nb`.
+
+        Takes into account both recovered and active drawdowns."""
+        drawdown = nb.dd_drawdown_nb(
+            self.resolve_field_arr('peak_val'),
+            self.resolve_field_arr('valley_val')
+        )
+        return self.map_array(drawdown)
 
     @cached_method
     def avg_drawdown(self, group_by: tp.GroupByLike = None,
                      wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
-        """Average drawdown (ADD)."""
+        """Average drawdown (ADD).
+
+        Based on `Drawdowns.drawdown`."""
         wrap_kwargs = merge_dicts(dict(name_or_index='avg_drawdown'), wrap_kwargs)
         return self.drawdown.mean(group_by=group_by, wrap_kwargs=wrap_kwargs, **kwargs)
 
     @cached_method
     def max_drawdown(self, group_by: tp.GroupByLike = None,
                      wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
-        """Maximum drawdown (MDD)."""
+        """Maximum drawdown (MDD).
+
+        Based on `Drawdowns.drawdown`."""
         wrap_kwargs = merge_dicts(dict(name_or_index='max_drawdown'), wrap_kwargs)
         return self.drawdown.min(group_by=group_by, wrap_kwargs=wrap_kwargs, **kwargs)
 
+    # ############# Recovery ############# #
+
     @cached_property
-    def duration(self) -> MappedArray:
-        """Duration of each drawdown (in raw format)."""
-        return self.map(nb.dd_duration_map_nb)
+    def recovery_return(self) -> MappedArray:
+        """See `vectorbt.generic.nb.dd_recovery_return_nb`.
 
-    @cached_method
-    def avg_duration(self, group_by: tp.GroupByLike = None,
-                     wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
-        """Average drawdown duration (in time units)."""
-        wrap_kwargs = merge_dicts(dict(to_timedelta=True, name_or_index='avg_duration'), wrap_kwargs)
-        return self.duration.mean(group_by=group_by, wrap_kwargs=wrap_kwargs, **kwargs)
+        Takes into account both recovered and active drawdowns."""
+        recovery_return = nb.dd_recovery_return_nb(
+            self.resolve_field_arr('valley_val'),
+            self.resolve_field_arr('end_val')
+        )
+        return self.map_array(recovery_return)
 
     @cached_method
-    def max_duration(self, group_by: tp.GroupByLike = None,
-                     wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
-        """Maximum drawdown duration (in time units)."""
-        wrap_kwargs = merge_dicts(dict(to_timedelta=True, name_or_index='max_duration'), wrap_kwargs)
-        return self.duration.max(group_by=group_by, wrap_kwargs=wrap_kwargs, **kwargs)
+    def avg_recovery_return(self, group_by: tp.GroupByLike = None,
+                            wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
+        """Average recovery return.
+
+        Based on `Drawdowns.recovery_return`."""
+        wrap_kwargs = merge_dicts(dict(name_or_index='avg_recovery_return'), wrap_kwargs)
+        return self.recovery_return.mean(group_by=group_by, wrap_kwargs=wrap_kwargs, **kwargs)
 
     @cached_method
-    def coverage(self, group_by: tp.GroupByLike = None,
-                 wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Coverage, that is, total duration divided by the whole period."""
-        total_duration = to_1d_array(self.duration.sum(group_by=group_by))
-        total_steps = self.wrapper.grouper.get_group_lens(group_by=group_by) * self.wrapper.shape[0]
-        wrap_kwargs = merge_dicts(dict(name_or_index='coverage'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(total_duration / total_steps, group_by=group_by, **wrap_kwargs)
+    def max_recovery_return(self, group_by: tp.GroupByLike = None,
+                            wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
+        """Maximum recovery return.
+
+        Based on `Drawdowns.recovery_return`."""
+        wrap_kwargs = merge_dicts(dict(name_or_index='max_recovery_return'), wrap_kwargs)
+        return self.recovery_return.max(group_by=group_by, wrap_kwargs=wrap_kwargs, **kwargs)
+
+    # ############# Duration ############# #
 
     @cached_property
     def decline_duration(self) -> MappedArray:
-        """Decline duration of each drawdown."""
-        return self.map(nb.dd_decline_duration_map_nb)
+        """See `vectorbt.generic.nb.dd_decline_duration_nb`.
 
-    @cached_property
-    def recovery_return(self) -> MappedArray:
-        """Recovery return of each drawdown."""
-        return self.map(nb.dd_recovery_return_map_nb, to_2d_array(self.ts))
+        Takes into account both recovered and active drawdowns."""
+        decline_duration = nb.dd_decline_duration_nb(
+            self.resolve_field_arr('start_idx'),
+            self.resolve_field_arr('valley_idx')
+        )
+        return self.map_array(decline_duration)
 
     @cached_property
     def recovery_duration(self) -> MappedArray:
-        """Recovery duration of each drawdown."""
-        return self.map(nb.dd_recovery_duration_map_nb)
+        """See `vectorbt.generic.nb.dd_recovery_duration_nb`.
 
-    @cached_property
-    def recovery_duration_ratio(self) -> MappedArray:
-        """Ratio of recovery duration to total duration of each drawdown.
-
-        The time from valley to recovery divided by the time from peak to valley."""
-        return self.map(nb.dd_recovery_duration_ratio_map_nb)
-
-    # ############# DrawdownStatus ############# #
+        A value higher than 1 means the recovery was slower than the decline.
 
-    @cached_property
-    def active(self: DrawdownsT) -> DrawdownsT:
-        """Active drawdowns."""
-        filter_mask = self.values['status'] == DrawdownStatus.Active
-        return self.filter_by_mask(filter_mask)
-
-    @cached_method
-    def active_rate(self, group_by: tp.GroupByLike = None,
-                    wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Rate of recovered drawdowns."""
-        active_count = to_1d_array(self.active.count(group_by=group_by))
-        total_count = to_1d_array(self.count(group_by=group_by))
-        wrap_kwargs = merge_dicts(dict(name_or_index='active_rate'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(active_count / total_count, group_by=group_by, **wrap_kwargs)
+        Takes into account both recovered and active drawdowns."""
+        recovery_duration = nb.dd_recovery_duration_nb(
+            self.resolve_field_arr('valley_idx'),
+            self.resolve_field_arr('end_idx')
+        )
+        return self.map_array(recovery_duration)
 
     @cached_property
-    def recovered(self: DrawdownsT) -> DrawdownsT:
-        """Recovered drawdowns."""
-        filter_mask = self.values['status'] == DrawdownStatus.Recovered
-        return self.filter_by_mask(filter_mask)
+    def recovery_duration_ratio(self) -> MappedArray:
+        """See `vectorbt.generic.nb.dd_recovery_duration_ratio_nb`.
 
-    @cached_method
-    def recovered_rate(self, group_by: tp.GroupByLike = None,
-                       wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Rate of recovered drawdowns."""
-        recovered_count = to_1d_array(self.recovered.count(group_by=group_by))
-        total_count = to_1d_array(self.count(group_by=group_by))
-        wrap_kwargs = merge_dicts(dict(name_or_index='recovered_rate'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(recovered_count / total_count, group_by=group_by, **wrap_kwargs)
+        Takes into account both recovered and active drawdowns."""
+        recovery_duration_ratio = nb.dd_recovery_duration_ratio_nb(
+            self.resolve_field_arr('start_idx'),
+            self.resolve_field_arr('valley_idx'),
+            self.resolve_field_arr('end_idx')
+        )
+        return self.map_array(recovery_duration_ratio)
 
-    # ############# Active drawdown ############# #
+    # ############# Status: Active ############# #
 
     @cached_method
     def active_drawdown(self, group_by: tp.GroupByLike = None,
                         wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Active drawdown from peak.
+        """Drawdown of the last active drawdown only.
 
         Does not support grouping."""
         if self.wrapper.grouper.is_grouped(group_by=group_by):
             raise ValueError("Grouping is not supported by this method")
-        curr_end_val = self.active.end_value.nth(-1, group_by=group_by)
-        curr_start_val = self.active.start_value.nth(-1, group_by=group_by)
-        curr_drawdown = (curr_end_val - curr_start_val) / curr_start_val
         wrap_kwargs = merge_dicts(dict(name_or_index='active_drawdown'), wrap_kwargs)
+        active = self.active
+        curr_end_val = active.end_val.nth(-1, group_by=group_by)
+        curr_peak_val = active.peak_val.nth(-1, group_by=group_by)
+        curr_drawdown = (curr_end_val - curr_peak_val) / curr_peak_val
         return self.wrapper.wrap_reduced(curr_drawdown, group_by=group_by, **wrap_kwargs)
 
     @cached_method
     def active_duration(self, group_by: tp.GroupByLike = None,
                         wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
-        """Active duration from peak.
+        """Duration of the last active drawdown only.
 
         Does not support grouping."""
         if self.wrapper.grouper.is_grouped(group_by=group_by):
@@ -394,22 +417,23 @@ def active_duration(self, group_by: tp.GroupByLike = None,
     @cached_method
     def active_recovery(self, group_by: tp.GroupByLike = None,
                         wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Active recovery.
+        """Recovery of the last active drawdown only.
 
         Does not support grouping."""
         if self.wrapper.grouper.is_grouped(group_by=group_by):
             raise ValueError("Grouping is not supported by this method")
-        curr_start_val = self.active.start_value.nth(-1, group_by=group_by)
-        curr_end_val = self.active.end_value.nth(-1, group_by=group_by)
-        curr_valley_val = self.active.valley_value.nth(-1, group_by=group_by)
-        curr_recovery = (curr_end_val - curr_valley_val) / (curr_start_val - curr_valley_val)
         wrap_kwargs = merge_dicts(dict(name_or_index='active_recovery'), wrap_kwargs)
+        active = self.active
+        curr_peak_val = active.peak_val.nth(-1, group_by=group_by)
+        curr_end_val = active.end_val.nth(-1, group_by=group_by)
+        curr_valley_val = active.valley_val.nth(-1, group_by=group_by)
+        curr_recovery = (curr_end_val - curr_valley_val) / (curr_peak_val - curr_valley_val)
         return self.wrapper.wrap_reduced(curr_recovery, group_by=group_by, **wrap_kwargs)
 
     @cached_method
     def active_recovery_return(self, group_by: tp.GroupByLike = None,
                                wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
-        """Active recovery return from valley.
+        """Recovery return of the last active drawdown only.
 
         Does not support grouping."""
         if self.wrapper.grouper.is_grouped(group_by=group_by):
@@ -420,7 +444,7 @@ def active_recovery_return(self, group_by: tp.GroupByLike = None,
     @cached_method
     def active_recovery_duration(self, group_by: tp.GroupByLike = None,
                                  wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
-        """Active recovery duration from valley.
+        """Recovery duration of the last active drawdown only.
 
         Does not support grouping."""
         if self.wrapper.grouper.is_grouped(group_by=group_by):
@@ -465,6 +489,12 @@ def stats_defaults(self) -> tp.Kwargs:
                 agg_func=None,
                 tags='wrapper'
             ),
+            coverage=dict(
+                title='Coverage [%]',
+                calc_func='coverage',
+                post_calc_func=lambda self, out, settings: out * 100,
+                tags=['ranges', 'duration']
+            ),
             total_records=dict(
                 title='Total Records',
                 calc_func='count',
@@ -581,7 +611,6 @@ def metrics(self) -> Config:
     def plot(self,
              column: tp.Optional[tp.Label] = None,
              top_n: int = 5,
-             plot_ts: bool = True,
              plot_zones: bool = True,
              ts_trace_kwargs: tp.KwargsLike = None,
              peak_trace_kwargs: tp.KwargsLike = None,
@@ -596,14 +625,13 @@ def plot(self,
              yref: str = 'y',
              fig: tp.Optional[tp.BaseFigure] = None,
              **layout_kwargs) -> tp.BaseFigure:  # pragma: no cover
-        """Plot drawdowns over `Drawdowns.ts`.
+        """Plot drawdowns.
 
         Args:
             column (str): Name of the column to plot.
             top_n (int): Filter top N drawdown records by maximum drawdown.
-            plot_ts (bool): Whether to plot time series.
             plot_zones (bool): Whether to plot zones.
-            ts_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for time series.
+            ts_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for `Drawdowns.ts`.
             peak_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for peak values.
             valley_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for valley values.
             recovery_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for recovery values.
@@ -621,13 +649,15 @@ def plot(self,
 
         ```python-repl
         >>> import vectorbt as vbt
+        >>> from datetime import datetime, timedelta
         >>> import pandas as pd
 
-        >>> ts = pd.Series([1, 2, 1, 2, 3, 2, 1, 2])
-        >>> vbt.Drawdowns.from_ts(ts, freq='1 days').plot()
+        >>> price = pd.Series([1, 2, 1, 2, 3, 2, 1, 2], name='Price')
+        >>> price.index = [datetime(2020, 1, 1) + timedelta(days=i) for i in range(len(price))]
+        >>> vbt.Drawdowns.from_ts(price, wrapper_kwargs=dict(freq='1 day')).plot()
         ```
 
-        ![](/vectorbt/docs/img/drawdowns_plot.svg)
+        ![](/docs/img/drawdowns_plot.svg)
         """
         from vectorbt._settings import settings
         plotting_cfg = settings['plotting']
@@ -635,7 +665,7 @@ def plot(self,
         self_col = self.select_one(column=column, group_by=False)
         if top_n is not None:
             # Drawdowns is negative, thus top_n becomes bottom_n
-            self_col = self_col.filter_by_mask(self_col.drawdown.bottom_n_mask(top_n))
+            self_col = self_col.apply_mask(self_col.drawdown.bottom_n_mask(top_n))
 
         if ts_trace_kwargs is None:
             ts_trace_kwargs = {}
@@ -666,37 +696,50 @@ def plot(self,
         fig.update_layout(**layout_kwargs)
         y_domain = get_domain(yref, fig)
 
-        if plot_ts:
+        if self_col.ts is not None:
             fig = self_col.ts.vbt.plot(trace_kwargs=ts_trace_kwargs, add_trace_kwargs=add_trace_kwargs, fig=fig)
 
-        if len(self_col.values) > 0:
+        if self_col.count() > 0:
             # Extract information
-            _id = self_col.values['id']
-            start_idx = self_col.values['start_idx']
-            valley_idx = self_col.values['valley_idx']
-            end_idx = self_col.values['end_idx']
-            status = self_col.values['status']
-
-            start_val = self_col.ts.values[start_idx]
-            valley_val = self_col.ts.values[valley_idx]
-            end_val = self_col.ts.values[end_idx]
-
-            def get_duration_str(from_idx, to_idx):
-                if isinstance(self_col.wrapper.index, DatetimeIndexes):
-                    duration = self_col.wrapper.index[to_idx] - self_col.wrapper.index[from_idx]
-                elif self_col.wrapper.freq is not None:
-                    duration = self_col.wrapper.to_timedelta(to_idx - from_idx)
-                else:
-                    duration = to_idx - from_idx
-                return np.vectorize(str)(duration)
-
-            # Plot peak markers
-            peak_mask = start_idx != np.roll(end_idx, 1)  # peak and recovery at same time -> recovery wins
-            if np.any(peak_mask):
-                peak_customdata = _id[peak_mask][:, None]
+            id_ = self_col.resolve_field_arr('id')
+            id_title = self_col.resolve_field_title('id')
+
+            peak_idx = self_col.resolve_map_field_to_index('peak_idx')
+            peak_idx_title = self_col.resolve_field_title('peak_idx')
+
+            peak_val = self_col.resolve_field_arr('peak_val')
+            peak_val_title = self_col.resolve_field_title('peak_val')
+
+            valley_idx = self_col.resolve_map_field_to_index('valley_idx')
+            valley_idx_title = self_col.resolve_field_title('valley_idx')
+
+            valley_val = self_col.resolve_field_arr('valley_val')
+            valley_val_title = self_col.resolve_field_title('valley_val')
+
+            end_idx = self_col.resolve_map_field_to_index('end_idx')
+            end_idx_title = self_col.resolve_field_title('end_idx')
+
+            end_val = self_col.resolve_field_arr('end_val')
+            end_val_title = self_col.resolve_field_title('end_val')
+
+            drawdown = self_col.drawdown.values
+            recovery_return = self_col.recovery_return.values
+            decline_duration = np.vectorize(str)(self_col.wrapper.to_timedelta(
+                self_col.decline_duration.values, to_pd=True, silence_warnings=True))
+            recovery_duration = np.vectorize(str)(self_col.wrapper.to_timedelta(
+                self_col.recovery_duration.values, to_pd=True, silence_warnings=True))
+            duration = np.vectorize(str)(self_col.wrapper.to_timedelta(
+                self_col.duration.values, to_pd=True, silence_warnings=True))
+
+            status = self_col.resolve_field_arr('status')
+
+            peak_mask = peak_idx != np.roll(end_idx, 1)  # peak and recovery at same time -> recovery wins
+            if peak_mask.any():
+                # Plot peak markers
+                peak_customdata = id_[peak_mask][:, None]
                 peak_scatter = go.Scatter(
-                    x=self_col.ts.index[start_idx[peak_mask]],
-                    y=start_val[peak_mask],
+                    x=peak_idx[peak_mask],
+                    y=peak_val[peak_mask],
                     mode='markers',
                     marker=dict(
                         symbol='diamond',
@@ -709,22 +752,24 @@ def get_duration_str(from_idx, to_idx):
                     ),
                     name='Peak',
                     customdata=peak_customdata,
-                    hovertemplate="Drawdown Id: %{customdata[0]}"
-                                  "<br>Date: %{x}"
-                                  "<br>Price: %{y}"
+                    hovertemplate=f"{id_title}: %{{customdata[0]}}"
+                                  f"<br>{peak_idx_title}: %{{x}}"
+                                  f"<br>{peak_val_title}: %{{y}}"
                 )
                 peak_scatter.update(**peak_trace_kwargs)
                 fig.add_trace(peak_scatter, **add_trace_kwargs)
 
-            recovery_mask = status == DrawdownStatus.Recovered
-            if np.any(recovery_mask):
+            recovered_mask = status == DrawdownStatus.Recovered
+            if recovered_mask.any():
                 # Plot valley markers
-                valley_drawdown = (valley_val[recovery_mask] - start_val[recovery_mask]) / start_val[recovery_mask]
-                valley_duration = get_duration_str(start_idx[recovery_mask], valley_idx[recovery_mask])
-                valley_customdata = np.stack((_id[recovery_mask], valley_drawdown, valley_duration), axis=1)
+                valley_customdata = np.stack((
+                    id_[recovered_mask],
+                    drawdown[recovered_mask],
+                    decline_duration[recovered_mask]
+                ), axis=1)
                 valley_scatter = go.Scatter(
-                    x=self_col.ts.index[valley_idx[recovery_mask]],
-                    y=valley_val[recovery_mask],
+                    x=valley_idx[recovered_mask],
+                    y=valley_val[recovered_mask],
                     mode='markers',
                     marker=dict(
                         symbol='diamond',
@@ -737,25 +782,25 @@ def get_duration_str(from_idx, to_idx):
                     ),
                     name='Valley',
                     customdata=valley_customdata,
-                    hovertemplate="Drawdown Id: %{customdata[0]}"
-                                  "<br>Date: %{x}"
-                                  "<br>Price: %{y}"
-                                  "<br>Drawdown: %{customdata[1]:.2%}"
-                                  "<br>Duration: %{customdata[2]}"
+                    hovertemplate=f"{id_title}: %{{customdata[0]}}"
+                                  f"<br>{valley_idx_title}: %{{x}}"
+                                  f"<br>{valley_val_title}: %{{y}}"
+                                  f"<br>Drawdown: %{{customdata[1]:.2%}}"
+                                  f"<br>Duration: %{{customdata[2]}}"
                 )
                 valley_scatter.update(**valley_trace_kwargs)
                 fig.add_trace(valley_scatter, **add_trace_kwargs)
 
                 if plot_zones:
                     # Plot drawdown zones
-                    for i in np.flatnonzero(recovery_mask):
+                    for i in range(len(id_[recovered_mask])):
                         fig.add_shape(**merge_dicts(dict(
                             type="rect",
                             xref=xref,
                             yref="paper",
-                            x0=self_col.ts.index[start_idx[i]],
+                            x0=peak_idx[recovered_mask][i],
                             y0=y_domain[0],
-                            x1=self_col.ts.index[valley_idx[i]],
+                            x1=valley_idx[recovered_mask][i],
                             y1=y_domain[1],
                             fillcolor='red',
                             opacity=0.2,
@@ -764,12 +809,14 @@ def get_duration_str(from_idx, to_idx):
                         ), decline_shape_kwargs))
 
                 # Plot recovery markers
-                recovery_return = (end_val[recovery_mask] - valley_val[recovery_mask]) / valley_val[recovery_mask]
-                recovery_duration = get_duration_str(valley_idx[recovery_mask], end_idx[recovery_mask])
-                recovery_customdata = np.stack((_id[recovery_mask], recovery_return, recovery_duration), axis=1)
+                recovery_customdata = np.stack((
+                    id_[recovered_mask],
+                    recovery_return[recovered_mask],
+                    recovery_duration[recovered_mask]
+                ), axis=1)
                 recovery_scatter = go.Scatter(
-                    x=self_col.ts.index[end_idx[recovery_mask]],
-                    y=end_val[recovery_mask],
+                    x=end_idx[recovered_mask],
+                    y=end_val[recovered_mask],
                     mode='markers',
                     marker=dict(
                         symbol='diamond',
@@ -782,25 +829,25 @@ def get_duration_str(from_idx, to_idx):
                     ),
                     name='Recovery/Peak',
                     customdata=recovery_customdata,
-                    hovertemplate="Drawdown Id: %{customdata[0]}"
-                                  "<br>Date: %{x}"
-                                  "<br>Price: %{y}"
-                                  "<br>Return: %{customdata[1]:.2%}"
-                                  "<br>Duration: %{customdata[2]}"
+                    hovertemplate=f"{id_title}: %{{customdata[0]}}"
+                                  f"<br>{end_idx_title}: %{{x}}"
+                                  f"<br>{end_val_title}: %{{y}}"
+                                  f"<br>Return: %{{customdata[1]:.2%}}"
+                                  f"<br>Duration: %{{customdata[2]}}"
                 )
                 recovery_scatter.update(**recovery_trace_kwargs)
                 fig.add_trace(recovery_scatter, **add_trace_kwargs)
 
                 if plot_zones:
                     # Plot recovery zones
-                    for i in np.flatnonzero(recovery_mask):
+                    for i in range(len(id_[recovered_mask])):
                         fig.add_shape(**merge_dicts(dict(
                             type="rect",
                             xref=xref,
                             yref="paper",
-                            x0=self_col.ts.index[valley_idx[i]],
+                            x0=valley_idx[recovered_mask][i],
                             y0=y_domain[0],
-                            x1=self_col.ts.index[end_idx[i]],
+                            x1=end_idx[recovered_mask][i],
                             y1=y_domain[1],
                             fillcolor='green',
                             opacity=0.2,
@@ -809,13 +856,15 @@ def get_duration_str(from_idx, to_idx):
                         ), recovery_shape_kwargs))
 
             # Plot active markers
-            active_mask = ~recovery_mask
-            if np.any(active_mask):
-                active_drawdown = (valley_val[active_mask] - start_val[active_mask]) / start_val[active_mask]
-                active_duration = get_duration_str(valley_idx[active_mask], end_idx[active_mask])
-                active_customdata = np.stack((_id[active_mask], active_drawdown, active_duration), axis=1)
+            active_mask = status == DrawdownStatus.Active
+            if active_mask.any():
+                active_customdata = np.stack((
+                    id_[active_mask],
+                    drawdown[active_mask],
+                    duration[active_mask]
+                ), axis=1)
                 active_scatter = go.Scatter(
-                    x=self_col.ts.index[end_idx[active_mask]],
+                    x=end_idx[active_mask],
                     y=end_val[active_mask],
                     mode='markers',
                     marker=dict(
@@ -829,25 +878,25 @@ def get_duration_str(from_idx, to_idx):
                     ),
                     name='Active',
                     customdata=active_customdata,
-                    hovertemplate="Drawdown Id: %{customdata[0]}"
-                                  "<br>Date: %{x}"
-                                  "<br>Price: %{y}"
-                                  "<br>Drawdown: %{customdata[1]:.2%}"
-                                  "<br>Duration: %{customdata[2]}"
+                    hovertemplate=f"{id_title}: %{{customdata[0]}}"
+                                  f"<br>{end_idx_title}: %{{x}}"
+                                  f"<br>{end_val_title}: %{{y}}"
+                                  f"<br>Return: %{{customdata[1]:.2%}}"
+                                  f"<br>Duration: %{{customdata[2]}}"
                 )
                 active_scatter.update(**active_trace_kwargs)
                 fig.add_trace(active_scatter, **add_trace_kwargs)
 
                 if plot_zones:
                     # Plot active drawdown zones
-                    for i in np.flatnonzero(active_mask):
+                    for i in range(len(id_[active_mask])):
                         fig.add_shape(**merge_dicts(dict(
                             type="rect",
                             xref=xref,
                             yref="paper",
-                            x0=self_col.ts.index[start_idx[i]],
+                            x0=peak_idx[active_mask][i],
                             y0=y_domain[0],
-                            x1=self_col.ts.index[end_idx[i]],
+                            x1=end_idx[active_mask][i],
                             y1=y_domain[1],
                             fillcolor='orange',
                             opacity=0.2,
@@ -858,4 +907,5 @@ def get_duration_str(from_idx, to_idx):
         return fig
 
 
+Drawdowns.override_field_config_doc(__pdoc__)
 Drawdowns.override_metrics_doc(__pdoc__)
diff --git a/vectorbt/generic/enums.py b/vectorbt/generic/enums.py
index ebc17759..a135a873 100644
--- a/vectorbt/generic/enums.py
+++ b/vectorbt/generic/enums.py
@@ -8,14 +8,32 @@
 from vectorbt.utils.docs import to_doc
 
 __all__ = [
+    'RangeStatus',
     'DrawdownStatus',
-    'drawdown_dt'
+    'drawdown_dt',
+    'range_dt'
 ]
 
 __pdoc__ = {}
 
 
-# ############# Records ############# #
+# ############# Enums ############# #
+
+class RangeStatusT(tp.NamedTuple):
+    Open: int
+    Closed: int
+
+
+RangeStatus = RangeStatusT(*range(2))
+"""_"""
+
+__pdoc__['RangeStatus'] = f"""Range status.
+
+```json
+{to_doc(RangeStatus)}
+```
+"""
+
 
 class DrawdownStatusT(tp.NamedTuple):
     Active: int
@@ -32,12 +50,34 @@ class DrawdownStatusT(tp.NamedTuple):
 ```
 """
 
+# ############# Records ############# #
+
+range_dt = np.dtype([
+    ('id', np.int_),
+    ('col', np.int_),
+    ('start_idx', np.int_),
+    ('end_idx', np.int_),
+    ('status', np.int_)
+], align=True)
+"""_"""
+
+__pdoc__['range_dt'] = f"""`np.dtype` of range records.
+
+```json
+{to_doc(range_dt)}
+```
+"""
+
 drawdown_dt = np.dtype([
     ('id', np.int_),
     ('col', np.int_),
+    ('peak_idx', np.int_),
     ('start_idx', np.int_),
     ('valley_idx', np.int_),
     ('end_idx', np.int_),
+    ('peak_val', np.float_),
+    ('valley_val', np.float_),
+    ('end_val', np.float_),
     ('status', np.int_),
 ], align=True)
 """_"""
diff --git a/vectorbt/generic/nb.py b/vectorbt/generic/nb.py
index b98789ec..52fa6efb 100644
--- a/vectorbt/generic/nb.py
+++ b/vectorbt/generic/nb.py
@@ -31,7 +31,7 @@
 from numba.core.types import Omitted
 
 from vectorbt import _typing as tp
-from vectorbt.generic.enums import DrawdownStatus, drawdown_dt
+from vectorbt.generic.enums import RangeStatus, DrawdownStatus, range_dt, drawdown_dt
 
 
 @njit(cache=True)
@@ -1406,6 +1406,171 @@ def any_squeeze_nb(col: int, group: int, a: tp.Array1d) -> bool:
     return np.any(a)
 
 
+# ############# Ranges ############# #
+
+@njit(cache=True)
+def find_ranges_nb(ts: tp.Array2d, gap_value: tp.Scalar) -> tp.RecordArray:
+    """Find ranges and store their information as records to an array.
+
+    ## Example
+
+    Find ranges in time series:
+    ```python-repl
+    >>> import numpy as np
+    >>> import pandas as pd
+    >>> from vectorbt.generic.nb import find_ranges_nb
+
+    >>> ts = np.asarray([
+    ...     [np.nan, np.nan, np.nan, np.nan],
+    ...     [     2, np.nan, np.nan, np.nan],
+    ...     [     3,      3, np.nan, np.nan],
+    ...     [np.nan,      4,      4, np.nan],
+    ...     [     5, np.nan,      5,      5],
+    ...     [     6,      6, np.nan,      6]
+    ... ])
+    >>> records = find_ranges_nb(ts, np.nan)
+
+    >>> pd.DataFrame.from_records(records)
+       id  col  start_idx  end_idx
+    0   0    0          1        3
+    1   1    0          4        6
+    2   2    1          2        4
+    3   3    1          5        6
+    4   4    2          3        5
+    5   5    3          4        6
+    ```
+    """
+    out = np.empty(ts.shape[0] * ts.shape[1], dtype=range_dt)
+    ridx = 0
+
+    for col in range(ts.shape[1]):
+        range_started = False
+        start_idx = -1
+        end_idx = -1
+        store_record = False
+        status = -1
+
+        for i in range(ts.shape[0]):
+            cur_val = ts[i, col]
+
+            if cur_val == gap_value or np.isnan(cur_val) and np.isnan(gap_value):
+                if range_started:
+                    # If stopped, save the current range
+                    end_idx = i
+                    range_started = False
+                    store_record = True
+                    status = RangeStatus.Closed
+            else:
+                if not range_started:
+                    # If started, register a new range
+                    start_idx = i
+                    range_started = True
+
+            if i == ts.shape[0] - 1 and range_started:
+                # If still running, mark for save
+                end_idx = ts.shape[0] - 1
+                range_started = False
+                store_record = True
+                status = RangeStatus.Open
+
+            if store_record:
+                # Save range to the records
+                out[ridx]['id'] = ridx
+                out[ridx]['col'] = col
+                out[ridx]['start_idx'] = start_idx
+                out[ridx]['end_idx'] = end_idx
+                out[ridx]['status'] = status
+                ridx += 1
+
+                # Reset running vars for a new range
+                store_record = False
+
+    return out[:ridx]
+
+
+@njit(cache=True)
+def range_duration_nb(start_idx_arr: tp.Array1d,
+                      end_idx_arr: tp.Array1d,
+                      status_arr: tp.Array2d) -> tp.Array1d:
+    """Get duration of each duration record."""
+    out = np.empty(start_idx_arr.shape[0], dtype=np.int_)
+    for ridx in range(out.shape[0]):
+        if status_arr[ridx] == RangeStatus.Open:
+            out[ridx] = end_idx_arr[ridx] - start_idx_arr[ridx] + 1
+        else:
+            out[ridx] = end_idx_arr[ridx] - start_idx_arr[ridx]
+    return out
+
+
+@njit(cache=True)
+def range_coverage_nb(start_idx_arr: tp.Array1d,
+                      end_idx_arr: tp.Array1d,
+                      status_arr: tp.Array2d,
+                      col_map: tp.ColMap,
+                      index_lens: tp.Array1d,
+                      overlapping: bool = False,
+                      normalize: bool = False) -> tp.Array1d:
+    """Get coverage of range records.
+
+    Set `overlapping` to True to get the number of overlapping steps.
+    Set `normalize` to True to get the number of steps in relation either to the total number of steps
+    (when `overlapping=False`) or to the number of covered steps (when `overlapping=True`).
+    """
+    col_idxs, col_lens = col_map
+    col_start_idxs = np.cumsum(col_lens) - col_lens
+    out = np.full(col_lens.shape[0], np.nan, dtype=np.float_)
+
+    for col in range(col_lens.shape[0]):
+        col_len = col_lens[col]
+        if col_len == 0:
+            continue
+        col_start_idx = col_start_idxs[col]
+        ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+        temp = np.full(index_lens[col], 0, dtype=np.int_)
+        for ridx in ridxs:
+            if status_arr[ridx] == RangeStatus.Open:
+                temp[start_idx_arr[ridx]:end_idx_arr[ridx] + 1] += 1
+            else:
+                temp[start_idx_arr[ridx]:end_idx_arr[ridx]] += 1
+        if overlapping:
+            if normalize:
+                out[col] = np.sum(temp > 1) / np.sum(temp > 0)
+            else:
+                out[col] = np.sum(temp > 1)
+        else:
+            if normalize:
+                out[col] = np.sum(temp > 0) / index_lens[col]
+            else:
+                out[col] = np.sum(temp > 0)
+    return out
+
+
+@njit(cache=True)
+def ranges_to_mask_nb(start_idx_arr: tp.Array1d,
+                      end_idx_arr: tp.Array1d,
+                      status_arr: tp.Array2d,
+                      col_map: tp.ColMap,
+                      index_len: int) -> tp.Array2d:
+    """Convert ranges to 2-dim mask."""
+    col_idxs, col_lens = col_map
+    col_start_idxs = np.cumsum(col_lens) - col_lens
+    out = np.full((index_len, col_lens.shape[0]), False, dtype=np.bool_)
+
+    for col in range(col_lens.shape[0]):
+        col_len = col_lens[col]
+        if col_len == 0:
+            continue
+        col_start_idx = col_start_idxs[col]
+        ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+        for ridx in ridxs:
+            if status_arr[ridx] == RangeStatus.Open:
+                out[start_idx_arr[ridx]:end_idx_arr[ridx] + 1, col] = True
+            else:
+                out[start_idx_arr[ridx]:end_idx_arr[ridx], col] = True
+
+    return out
+
+
 # ############# Drawdowns ############# #
 
 @njit(cache=True)
@@ -1430,10 +1595,15 @@ def find_drawdowns_nb(ts: tp.Array2d) -> tp.RecordArray:
     >>> records = find_drawdowns_nb(ts)
 
     >>> pd.DataFrame.from_records(records)
-       id  col  start_idx  valley_idx  end_idx  status
-    0   0    1          0           4        4       0
-    1   1    2          2           4        4       0
-    2   2    3          0           2        4       1
+       id  col  peak_idx  start_idx  valley_idx  end_idx  peak_val  valley_val  \\
+    0   0    1         0          1           4        4       5.0         1.0
+    1   1    2         2          3           4        4       3.0         1.0
+    2   2    3         0          1           2        4       3.0         1.0
+
+       end_val  status
+    0      1.0       0
+    1      1.0       0
+    2      3.0       1
     ```
     """
     out = np.empty(ts.shape[0] * ts.shape[1], dtype=drawdown_dt)
@@ -1441,11 +1611,12 @@ def find_drawdowns_nb(ts: tp.Array2d) -> tp.RecordArray:
 
     for col in range(ts.shape[1]):
         drawdown_started = False
-        peak_idx = np.nan
-        valley_idx = np.nan
+        peak_idx = -1
+        valley_idx = -1
         peak_val = ts[0, col]
         valley_val = ts[0, col]
-        store_drawdown = False
+        end_val = np.nan
+        store_record = False
         status = -1
 
         for i in range(ts.shape[0]):
@@ -1462,7 +1633,8 @@ def find_drawdowns_nb(ts: tp.Array2d) -> tp.RecordArray:
                         # If running, potential recovery
                         if cur_val >= peak_val:
                             drawdown_started = False
-                            store_drawdown = True
+                            store_record = True
+                            end_val = peak_val
                             status = DrawdownStatus.Recovered
                 else:
                     # Value decreased
@@ -1480,16 +1652,21 @@ def find_drawdowns_nb(ts: tp.Array2d) -> tp.RecordArray:
                 if i == ts.shape[0] - 1 and drawdown_started:
                     # If still running, mark for save
                     drawdown_started = False
-                    store_drawdown = True
+                    store_record = True
+                    end_val = cur_val
                     status = DrawdownStatus.Active
 
-                if store_drawdown:
+                if store_record:
                     # Save drawdown to the records
                     out[ddidx]['id'] = ddidx
                     out[ddidx]['col'] = col
-                    out[ddidx]['start_idx'] = peak_idx
+                    out[ddidx]['peak_idx'] = peak_idx
+                    out[ddidx]['start_idx'] = peak_idx + 1
                     out[ddidx]['valley_idx'] = valley_idx
                     out[ddidx]['end_idx'] = i
+                    out[ddidx]['peak_val'] = peak_val
+                    out[ddidx]['valley_val'] = valley_val
+                    out[ddidx]['end_val'] = end_val
                     out[ddidx]['status'] = status
                     ddidx += 1
 
@@ -1498,67 +1675,42 @@ def find_drawdowns_nb(ts: tp.Array2d) -> tp.RecordArray:
                     valley_idx = i
                     peak_val = cur_val
                     valley_val = cur_val
-                    store_drawdown = False
+                    store_record = False
                     status = -1
 
     return out[:ddidx]
 
 
 @njit(cache=True)
-def dd_start_value_map_nb(record: tp.Record, ts: tp.Array2d) -> float:
-    """`map_func_nb` that returns start value of a drawdown."""
-    return ts[record['start_idx'], record['col']]
-
-
-@njit(cache=True)
-def dd_valley_value_map_nb(record: tp.Record, ts: tp.Array2d) -> float:
-    """`map_func_nb` that returns valley value of a drawdown."""
-    return ts[record['valley_idx'], record['col']]
-
-
-@njit(cache=True)
-def dd_end_value_map_nb(record: tp.Record, ts: tp.Array2d) -> float:
-    """`map_func_nb` that returns end value of a drawdown.
-
-    This can be either recovery value or last value of an active drawdown."""
-    return ts[record['end_idx'], record['col']]
-
-
-@njit(cache=True)
-def dd_drawdown_map_nb(record: tp.Record, ts: tp.Array2d) -> float:
-    """`map_func_nb` that returns drawdown value of a drawdown."""
-    valley_val = dd_valley_value_map_nb(record, ts)
-    start_val = dd_start_value_map_nb(record, ts)
-    return (valley_val - start_val) / start_val
-
-
-@njit(cache=True)
-def dd_duration_map_nb(record: tp.Record) -> int:
-    """`map_func_nb` that returns total duration of a drawdown."""
-    return record['end_idx'] - record['start_idx']
+def dd_drawdown_nb(peak_val_arr: tp.Array1d, valley_val_arr: tp.Array1d) -> tp.Array1d:
+    """Return the drawdown of each drawdown record."""
+    return (valley_val_arr - peak_val_arr) / peak_val_arr
 
 
 @njit(cache=True)
-def dd_decline_duration_map_nb(record: tp.Record) -> int:
-    """`map_func_nb` that returns duration of the peak-to-valley phase."""
-    return record['valley_idx'] - record['start_idx']
+def dd_decline_duration_nb(start_idx_arr: tp.Array1d, valley_idx_arr: tp.Array1d) -> tp.Array1d:
+    """Return the duration of the peak-to-valley phase of each drawdown record."""
+    return valley_idx_arr - start_idx_arr + 1
 
 
 @njit(cache=True)
-def dd_recovery_duration_map_nb(record: tp.Record) -> int:
-    """`map_func_nb` that returns duration of the valley-to-recovery phase."""
-    return record['end_idx'] - record['valley_idx']
+def dd_recovery_duration_nb(valley_idx_arr: tp.Array1d,
+                            end_idx_arr: tp.Array1d) -> tp.Array1d:
+    """Return the duration of the valley-to-recovery phase of each drawdown record."""
+    return end_idx_arr - valley_idx_arr
 
 
 @njit(cache=True)
-def dd_recovery_duration_ratio_map_nb(record: tp.Record) -> float:
-    """`map_func_nb` that returns ratio of recovery duration to total duration."""
-    return dd_recovery_duration_map_nb(record) / dd_duration_map_nb(record)
+def dd_recovery_duration_ratio_nb(start_idx_arr: tp.Array1d,
+                                  valley_idx_arr: tp.Array1d,
+                                  end_idx_arr: tp.Array1d) -> tp.Array1d:
+    """Return the ratio of the recovery duration to the decline duration of each drawdown record."""
+    recovery_duration = dd_recovery_duration_nb(valley_idx_arr, end_idx_arr)
+    decline_duration = dd_decline_duration_nb(start_idx_arr, valley_idx_arr)
+    return recovery_duration / decline_duration
 
 
 @njit(cache=True)
-def dd_recovery_return_map_nb(record: tp.Record, ts: tp.Array2d) -> float:
-    """`map_func_nb` that returns recovery return of a drawdown."""
-    end_val = dd_end_value_map_nb(record, ts)
-    valley_val = dd_valley_value_map_nb(record, ts)
-    return (end_val - valley_val) / valley_val
+def dd_recovery_return_nb(valley_val_arr: tp.Array1d, end_val_arr: tp.Array1d) -> tp.Array1d:
+    """Return the recovery return of each drawdown record."""
+    return (end_val_arr - valley_val_arr) / valley_val_arr
diff --git a/vectorbt/generic/plot_builder.py b/vectorbt/generic/plot_builder.py
index e914c49d..4081e262 100644
--- a/vectorbt/generic/plot_builder.py
+++ b/vectorbt/generic/plot_builder.py
@@ -30,7 +30,7 @@ class PlotBuilderMixin(metaclass=MetaPlotBuilderMixin):
     Required to be a subclass of `vectorbt.base.array_wrapper.Wrapping`."""
 
     def __init__(self):
-        checks.assert_type(self, Wrapping)
+        checks.assert_instance_of(self, Wrapping)
 
         # Copy writeable attrs
         self._subplots = self.__class__._subplots.copy()
@@ -60,7 +60,7 @@ def plot_defaults(self) -> tp.Kwargs:
 
     @property
     def subplots(self) -> Config:
-        """Subplots supported by `PlotBuilderMixin.plot`.
+        """Subplots supported by `${cls_name}`.
 
         ```json
         ${subplots}
@@ -663,6 +663,8 @@ def _getattr_func(obj: tp.Any,
         # Return the figure
         return fig
 
+    # ############# Docs ############# #
+
     @classmethod
     def build_subplots_doc(cls, source_cls: tp.Optional[type] = None) -> str:
         """Build subplots documentation."""
@@ -671,7 +673,7 @@ def build_subplots_doc(cls, source_cls: tp.Optional[type] = None) -> str:
         return string.Template(
             inspect.cleandoc(get_dict_attr(source_cls, 'subplots').__doc__)
         ).substitute(
-            {'subplots': cls.subplots.to_doc()}
+            {'subplots': cls.subplots.to_doc(), 'cls_name': cls.__name__}
         )
 
     @classmethod
diff --git a/vectorbt/generic/plotting.py b/vectorbt/generic/plotting.py
index 687422f2..f6c02168 100644
--- a/vectorbt/generic/plotting.py
+++ b/vectorbt/generic/plotting.py
@@ -93,7 +93,7 @@ def __init__(self,
         ... )
         >>> gauge.fig
         ```
-        ![](/vectorbt/docs/img/Gauge.svg)
+        ![](/docs/img/Gauge.svg)
         """
         Configured.__init__(
             self,
@@ -193,7 +193,7 @@ def __init__(self,
         ... )
         >>> bar.fig
         ```
-        ![](/vectorbt/docs/img/Bar.svg)
+        ![](/docs/img/Bar.svg)
         """
         Configured.__init__(
             self,
@@ -252,7 +252,7 @@ def update(self, data: tp.ArrayLike) -> None:
         >>> bar.update([[2, 1], [4, 3]])
         >>> bar.fig
         ```
-        ![](/vectorbt/docs/img/Bar_updated.svg)
+        ![](/docs/img/Bar_updated.svg)
         """
         data = reshape_fns.to_2d(np.asarray(data))
         with self.fig.batch_update():
@@ -298,7 +298,7 @@ def __init__(self,
         ... )
         >>> scatter.fig
         ```
-        ![](/vectorbt/docs/img/Scatter.svg)
+        ![](/docs/img/Scatter.svg)
         """
         Configured.__init__(
             self,
@@ -402,7 +402,7 @@ def __init__(self,
         ... )
         >>> hist.fig
         ```
-        ![](/vectorbt/docs/img/Histogram.svg)
+        ![](/docs/img/Histogram.svg)
         """
         Configured.__init__(
             self,
@@ -508,7 +508,7 @@ def __init__(self,
         ... )
         >>> box.fig
         ```
-        ![](/vectorbt/docs/img/Box.svg)
+        ![](/docs/img/Box.svg)
         """
         Configured.__init__(
             self,
@@ -623,7 +623,7 @@ def __init__(self,
         ... )
         >>> heatmap.fig
         ```
-        ![](/vectorbt/docs/img/Heatmap.svg)
+        ![](/docs/img/Heatmap.svg)
         """
         Configured.__init__(
             self,
@@ -762,7 +762,7 @@ def __init__(self,
         >>> volume.fig
         ```
 
-        ![](/vectorbt/docs/img/Volume.svg)
+        ![](/docs/img/Volume.svg)
         """
         Configured.__init__(
             self,
diff --git a/vectorbt/generic/ranges.py b/vectorbt/generic/ranges.py
new file mode 100644
index 00000000..c262780f
--- /dev/null
+++ b/vectorbt/generic/ranges.py
@@ -0,0 +1,614 @@
+"""Base class for working with range records.
+
+Range records capture information on ranges. They are useful for analyzing duration of processes,
+such as drawdowns, trades, and positions. They also come in handy when analyzing distance between events,
+such as entry and exit signals.
+
+Each range has a starting point and an ending point. For example, the points for `range(20)`
+are 0 and 20 (not 19!) respectively.
+
+!!! note
+    Be aware that if a range hasn't ended in a column, its `end_idx` will point at the latest index.
+    Make sure to account for this when computing custom metrics involving duration.
+
+```python-repl
+>>> import vectorbt as vbt
+>>> import numpy as np
+>>> import pandas as pd
+
+>>> start = '2019-01-01 UTC'  # crypto is in UTC
+>>> end = '2020-01-01 UTC'
+>>> price = vbt.YFData.download('BTC-USD', start=start, end=end).get('Close')
+>>> fast_ma = vbt.MA.run(price, 10)
+>>> slow_ma = vbt.MA.run(price, 50)
+>>> fast_below_slow = fast_ma.ma_above(slow_ma)
+
+>>> ranges = vbt.Ranges.from_ts(fast_below_slow, wrapper_kwargs=dict(freq='d'))
+
+>>> ranges.records_readable
+   Range Id  Column                Start Date                  End Date  \\
+0         0       0 2019-07-25 00:00:00+00:00 2019-08-08 00:00:00+00:00
+1         1       0 2019-08-19 00:00:00+00:00 2019-11-01 00:00:00+00:00
+2         2       0 2019-11-20 00:00:00+00:00 2020-01-01 00:00:00+00:00
+
+   Status
+0  Closed
+1  Closed
+2    Open
+
+>>> ranges.duration.max(wrap_kwargs=dict(to_timedelta=True))
+Timedelta('74 days 00:00:00')
+```
+
+## From accessors
+
+Moreover, all generic accessors have a property `ranges` and a method `get_ranges`:
+
+```python-repl
+>>> # vectorbt.generic.accessors.GenericAccessor.ranges.coverage
+>>> fast_below_slow.vbt.ranges.coverage()
+0.35792349726775957
+```
+
+## Stats
+
+!!! hint
+    See `vectorbt.generic.stats_builder.StatsBuilderMixin.stats` and `Ranges.metrics`.
+
+```python-repl
+>>> df = pd.DataFrame({
+...     'a': [1, 2, np.nan, np.nan, 5, 6],
+...     'b': [np.nan, 2, np.nan, 4, np.nan, 6]
+... })
+>>> ranges = df.vbt(freq='d').ranges
+
+>>> ranges['a'].stats()
+Start                             0
+End                               5
+Period              6 days 00:00:00
+Total Records                     2
+Coverage            4 days 00:00:00
+Overlap Coverage    0 days 00:00:00
+Duration: Min       2 days 00:00:00
+Duration: Median    2 days 00:00:00
+Duration: Max       2 days 00:00:00
+Duration: Mean      2 days 00:00:00
+Duration: Std       0 days 00:00:00
+Name: a, dtype: object
+```
+
+`Ranges.stats` also supports (re-)grouping:
+
+```python-repl
+>>> ranges.stats(group_by=True)
+Start                                       0
+End                                         5
+Period                        6 days 00:00:00
+Total Records                               5
+Coverage                      5 days 00:00:00
+Overlap Coverage              2 days 00:00:00
+Duration: Min                 1 days 00:00:00
+Duration: Median              1 days 00:00:00
+Duration: Max                 2 days 00:00:00
+Duration: Mean                1 days 09:36:00
+Duration: Std       0 days 13:08:43.228968446
+Name: group, dtype: object
+
+```
+"""
+
+import numpy as np
+import plotly.graph_objects as go
+
+from vectorbt import _typing as tp
+from vectorbt.utils.decorators import cached_property, cached_method
+from vectorbt.utils.config import merge_dicts, Config
+from vectorbt.utils.colors import adjust_lightness
+from vectorbt.utils.figure import make_figure, get_domain
+from vectorbt.base.reshape_fns import to_pd_array, to_2d_array
+from vectorbt.base.array_wrapper import ArrayWrapper
+from vectorbt.generic.enums import RangeStatus, range_dt
+from vectorbt.generic.stats_builder import StatsBuilderMixin
+from vectorbt.generic import nb
+from vectorbt.records.base import Records
+from vectorbt.records.mapped_array import MappedArray
+from vectorbt.records.decorators import override_field_config, attach_fields
+
+__pdoc__ = {}
+
+ranges_field_config = Config(
+    dict(
+        dtype=range_dt,
+        settings=dict(
+            id=dict(
+                title='Range Id'
+            ),
+            idx=dict(
+                name='end_idx'  # remap field of Records
+            ),
+            start_idx=dict(
+                title='Start Date',
+                mapping='index'
+            ),
+            end_idx=dict(
+                title='End Date',
+                mapping='index'
+            ),
+            status=dict(
+                title='Status',
+                mapping=RangeStatus
+            )
+        )
+    ),
+    readonly=True,
+    as_attrs=False
+)
+"""_"""
+
+__pdoc__['ranges_field_config'] = f"""Field config for `Ranges`.
+
+```json
+{ranges_field_config.to_doc()}
+```
+"""
+
+ranges_attach_field_config = Config(
+    dict(
+        status=dict(
+            attach_filters=True
+        )
+    ),
+    readonly=True,
+    as_attrs=False
+)
+"""_"""
+
+__pdoc__['ranges_attach_field_config'] = f"""Config of fields to be attached to `Ranges`.
+
+```json
+{ranges_attach_field_config.to_doc()}
+```
+"""
+
+RangesT = tp.TypeVar("RangesT", bound="Ranges")
+
+
+@attach_fields(ranges_attach_field_config)
+@override_field_config(ranges_field_config)
+class Ranges(Records):
+    """Extends `Records` for working with range records.
+
+    Requires `records_arr` to have all fields defined in `vectorbt.generic.enums.range_dt`."""
+
+    @property
+    def field_config(self) -> Config:
+        return self._field_config
+
+    def __init__(self,
+                 wrapper: ArrayWrapper,
+                 records_arr: tp.RecordArray,
+                 ts: tp.Optional[tp.ArrayLike] = None,
+                 **kwargs) -> None:
+        Records.__init__(
+            self,
+            wrapper,
+            records_arr,
+            ts=ts,
+            **kwargs
+        )
+        self._ts = ts
+
+    def indexing_func(self: RangesT, pd_indexing_func: tp.PandasIndexingFunc, **kwargs) -> RangesT:
+        """Perform indexing on `Ranges`."""
+        new_wrapper, new_records_arr, _, col_idxs = \
+            Records.indexing_func_meta(self, pd_indexing_func, **kwargs)
+        if self.ts is not None:
+            new_ts = new_wrapper.wrap(self.ts.values[:, col_idxs], group_by=False)
+        else:
+            new_ts = None
+        return self.copy(
+            wrapper=new_wrapper,
+            records_arr=new_records_arr,
+            ts=new_ts
+        )
+
+    @classmethod
+    def from_ts(cls: tp.Type[RangesT],
+                ts: tp.ArrayLike,
+                gap_value: tp.Optional[tp.Scalar] = None,
+                wrapper_kwargs: tp.KwargsLike = None,
+                **kwargs) -> RangesT:
+        """Build `Ranges` from time series `ts`.
+
+        Searches for sequences of
+
+        * True values in boolean data (False acts as a gap),
+        * positive values in integer data (-1 acts as a gap), and
+        * non-NaN values in any other data (NaN acts as a gap).
+
+        `**kwargs` will be passed to `Ranges.__init__`."""
+        if wrapper_kwargs is None:
+            wrapper_kwargs = {}
+
+        ts_pd = to_pd_array(ts)
+        ts_arr = to_2d_array(ts_pd)
+        if gap_value is None:
+            if np.issubdtype(ts_arr.dtype, np.bool_):
+                gap_value = False
+            elif np.issubdtype(ts_arr.dtype, np.integer):
+                gap_value = -1
+            else:
+                gap_value = np.nan
+        records_arr = nb.find_ranges_nb(ts_arr, gap_value)
+        wrapper = ArrayWrapper.from_obj(ts_pd, **wrapper_kwargs)
+        return cls(wrapper, records_arr, ts=ts_pd, **kwargs)
+
+    @property
+    def ts(self) -> tp.Optional[tp.SeriesFrame]:
+        """Original time series that records are built from (optional)."""
+        return self._ts
+
+    def to_mask(self, group_by: tp.GroupByLike = None, wrap_kwargs: tp.KwargsLike = None) -> tp.SeriesFrame:
+        """Convert ranges to a mask.
+
+        See `vectorbt.generic.nb.ranges_to_mask_nb`."""
+        col_map = self.col_mapper.get_col_map(group_by=group_by)
+        mask = nb.ranges_to_mask_nb(
+            self.resolve_field_arr('start_idx'),
+            self.resolve_field_arr('end_idx'),
+            self.resolve_field_arr('status'),
+            col_map,
+            len(self.wrapper.index)
+        )
+        return self.wrapper.wrap(mask, group_by=group_by, **merge_dicts({}, wrap_kwargs))
+
+    @cached_property
+    def duration(self) -> MappedArray:
+        """Duration of each range (in raw format)."""
+        duration = nb.range_duration_nb(
+            self.resolve_field_arr('start_idx'),
+            self.resolve_field_arr('end_idx'),
+            self.resolve_field_arr('status')
+        )
+        return self.map_array(duration)
+
+    @cached_method
+    def avg_duration(self, group_by: tp.GroupByLike = None,
+                     wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
+        """Average range duration (as timedelta)."""
+        wrap_kwargs = merge_dicts(dict(to_timedelta=True, name_or_index='avg_duration'), wrap_kwargs)
+        return self.duration.mean(group_by=group_by, wrap_kwargs=wrap_kwargs, **kwargs)
+
+    @cached_method
+    def max_duration(self, group_by: tp.GroupByLike = None,
+                     wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
+        """Maximum range duration (as timedelta)."""
+        wrap_kwargs = merge_dicts(dict(to_timedelta=True, name_or_index='max_duration'), wrap_kwargs)
+        return self.duration.max(group_by=group_by, wrap_kwargs=wrap_kwargs, **kwargs)
+
+    @cached_method
+    def coverage(self,
+                 overlapping: bool = False,
+                 normalize: bool = True,
+                 group_by: tp.GroupByLike = None,
+                 wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
+        """Coverage, that is, the number of steps that are covered by all ranges.
+
+        See `vectorbt.generic.nb.range_coverage_nb`."""
+        col_map = self.col_mapper.get_col_map(group_by=group_by)
+        index_lens = self.wrapper.grouper.get_group_lens(group_by=group_by) * self.wrapper.shape[0]
+        coverage = nb.range_coverage_nb(
+            self.resolve_field_arr('start_idx'),
+            self.resolve_field_arr('end_idx'),
+            self.resolve_field_arr('status'),
+            col_map,
+            index_lens,
+            overlapping=overlapping,
+            normalize=normalize
+        )
+        wrap_kwargs = merge_dicts(dict(name_or_index='coverage'), wrap_kwargs)
+        return self.wrapper.wrap_reduced(coverage, group_by=group_by, **wrap_kwargs)
+
+    # ############# Stats ############# #
+
+    @property
+    def stats_defaults(self) -> tp.Kwargs:
+        """Defaults for `Ranges.stats`.
+
+        Merges `vectorbt.generic.stats_builder.StatsBuilderMixin.stats_defaults` and
+        `ranges.stats` in `vectorbt._settings.settings`."""
+        from vectorbt._settings import settings
+        ranges_stats_cfg = settings['ranges']['stats']
+
+        return merge_dicts(
+            StatsBuilderMixin.stats_defaults.__get__(self),
+            ranges_stats_cfg
+        )
+
+    _metrics: tp.ClassVar[Config] = Config(
+        dict(
+            start=dict(
+                title='Start',
+                calc_func=lambda self: self.wrapper.index[0],
+                agg_func=None,
+                tags='wrapper'
+            ),
+            end=dict(
+                title='End',
+                calc_func=lambda self: self.wrapper.index[-1],
+                agg_func=None,
+                tags='wrapper'
+            ),
+            period=dict(
+                title='Period',
+                calc_func=lambda self: len(self.wrapper.index),
+                apply_to_timedelta=True,
+                agg_func=None,
+                tags='wrapper'
+            ),
+            coverage=dict(
+                title='Coverage',
+                calc_func='coverage',
+                overlapping=False,
+                normalize=False,
+                apply_to_timedelta=True,
+                tags=['ranges', 'coverage']
+            ),
+            overlap_coverage=dict(
+                title='Overlap Coverage',
+                calc_func='coverage',
+                overlapping=True,
+                normalize=False,
+                apply_to_timedelta=True,
+                tags=['ranges', 'coverage']
+            ),
+            total_records=dict(
+                title='Total Records',
+                calc_func='count',
+                tags='records'
+            ),
+            duration=dict(
+                title='Duration',
+                calc_func='duration.describe',
+                post_calc_func=lambda self, out, settings: {
+                    'Min': out.loc['min'],
+                    'Median': out.loc['50%'],
+                    'Max': out.loc['max'],
+                    'Mean': out.loc['mean'],
+                    'Std': out.loc['std']
+                },
+                apply_to_timedelta=True,
+                tags=['ranges', 'duration']
+            ),
+        ),
+        copy_kwargs=dict(copy_mode='deep')
+    )
+
+    @property
+    def metrics(self) -> Config:
+        return self._metrics
+
+    # ############# Plotting ############# #
+
+    def plot(self,
+             column: tp.Optional[tp.Label] = None,
+             top_n: int = 5,
+             plot_zones: bool = True,
+             ts_trace_kwargs: tp.KwargsLike = None,
+             start_trace_kwargs: tp.KwargsLike = None,
+             end_trace_kwargs: tp.KwargsLike = None,
+             open_shape_kwargs: tp.KwargsLike = None,
+             closed_shape_kwargs: tp.KwargsLike = None,
+             add_trace_kwargs: tp.KwargsLike = None,
+             xref: str = 'x',
+             yref: str = 'y',
+             fig: tp.Optional[tp.BaseFigure] = None,
+             **layout_kwargs) -> tp.BaseFigure:  # pragma: no cover
+        """Plot ranges.
+
+        Args:
+            column (str): Name of the column to plot.
+            top_n (int): Filter top N range records by maximum duration.
+            plot_zones (bool): Whether to plot zones.
+            ts_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for `Ranges.ts`.
+            start_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for start values.
+            end_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for end values.
+            open_shape_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Figure.add_shape` for open zones.
+            closed_shape_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Figure.add_shape` for closed zones.
+            add_trace_kwargs (dict): Keyword arguments passed to `add_trace`.
+            xref (str): X coordinate axis.
+            yref (str): Y coordinate axis.
+            fig (Figure or FigureWidget): Figure to add traces to.
+            **layout_kwargs: Keyword arguments for layout.
+
+        ## Example
+
+        ```python-repl
+        >>> import vectorbt as vbt
+        >>> from datetime import datetime, timedelta
+        >>> import pandas as pd
+
+        >>> price = pd.Series([1, 2, 1, 2, 3, 2, 1, 2], name='Price')
+        >>> price.index = [datetime(2020, 1, 1) + timedelta(days=i) for i in range(len(price))]
+        >>> vbt.Ranges.from_ts(price >= 2, wrapper_kwargs=dict(freq='1 day')).plot()
+        ```
+
+        ![](/docs/img/ranges_plot.svg)
+        """
+        from vectorbt._settings import settings
+        plotting_cfg = settings['plotting']
+
+        self_col = self.select_one(column=column, group_by=False)
+        if top_n is not None:
+            self_col = self_col.apply_mask(self_col.duration.top_n_mask(top_n))
+
+        if ts_trace_kwargs is None:
+            ts_trace_kwargs = {}
+        ts_trace_kwargs = merge_dicts(dict(
+            line=dict(
+                color=plotting_cfg['color_schema']['blue']
+            )
+        ), ts_trace_kwargs)
+        if start_trace_kwargs is None:
+            start_trace_kwargs = {}
+        if end_trace_kwargs is None:
+            end_trace_kwargs = {}
+        if open_shape_kwargs is None:
+            open_shape_kwargs = {}
+        if closed_shape_kwargs is None:
+            closed_shape_kwargs = {}
+        if add_trace_kwargs is None:
+            add_trace_kwargs = {}
+
+        if fig is None:
+            fig = make_figure()
+        fig.update_layout(**layout_kwargs)
+        y_domain = get_domain(yref, fig)
+
+        if self_col.ts is not None:
+            fig = self_col.ts.vbt.plot(trace_kwargs=ts_trace_kwargs, add_trace_kwargs=add_trace_kwargs, fig=fig)
+
+        if self_col.count() > 0:
+            # Extract information
+            id_ = self_col.resolve_field_arr('id')
+            id_title = self_col.resolve_field_title('id')
+
+            start_idx = self_col.resolve_map_field_to_index('start_idx')
+            start_idx_title = self_col.resolve_field_title('start_idx')
+            if self_col.ts is not None:
+                start_val = self_col.ts.loc[start_idx]
+            else:
+                start_val = np.full(len(start_idx), 0)
+
+            end_idx = self_col.resolve_map_field_to_index('end_idx')
+            end_idx_title = self_col.resolve_field_title('end_idx')
+            if self_col.ts is not None:
+                end_val = self_col.ts.loc[end_idx]
+            else:
+                end_val = np.full(len(end_idx), 0)
+
+            duration = np.vectorize(str)(self_col.wrapper.to_timedelta(
+                self_col.duration.values, to_pd=True, silence_warnings=True))
+
+            status = self_col.resolve_field_arr('status')
+
+            # Plot start markers
+            start_customdata = id_[:, None]
+            start_scatter = go.Scatter(
+                x=start_idx,
+                y=start_val,
+                mode='markers',
+                marker=dict(
+                    symbol='diamond',
+                    color=plotting_cfg['contrast_color_schema']['blue'],
+                    size=7,
+                    line=dict(
+                        width=1,
+                        color=adjust_lightness(plotting_cfg['contrast_color_schema']['blue'])
+                    )
+                ),
+                name='Start',
+                customdata=start_customdata,
+                hovertemplate=f"{id_title}: %{{customdata[0]}}"
+                              f"<br>{start_idx_title}: %{{x}}"
+            )
+            start_scatter.update(**start_trace_kwargs)
+            fig.add_trace(start_scatter, **add_trace_kwargs)
+
+            closed_mask = status == RangeStatus.Closed
+            if closed_mask.any():
+                # Plot end markers
+                closed_end_customdata = np.stack((
+                    id_[closed_mask],
+                    duration[closed_mask]
+                ), axis=1)
+                closed_end_scatter = go.Scatter(
+                    x=end_idx[closed_mask],
+                    y=end_val[closed_mask],
+                    mode='markers',
+                    marker=dict(
+                        symbol='diamond',
+                        color=plotting_cfg['contrast_color_schema']['green'],
+                        size=7,
+                        line=dict(
+                            width=1,
+                            color=adjust_lightness(plotting_cfg['contrast_color_schema']['green'])
+                        )
+                    ),
+                    name='Closed',
+                    customdata=closed_end_customdata,
+                    hovertemplate=f"{id_title}: %{{customdata[0]}}"
+                                  f"<br>{end_idx_title}: %{{x}}"
+                                  f"<br>Duration: %{{customdata[1]}}"
+                )
+                closed_end_scatter.update(**end_trace_kwargs)
+                fig.add_trace(closed_end_scatter, **add_trace_kwargs)
+
+                if plot_zones:
+                    # Plot closed range zones
+                    for i in range(len(id_[closed_mask])):
+                        fig.add_shape(**merge_dicts(dict(
+                            type="rect",
+                            xref=xref,
+                            yref="paper",
+                            x0=start_idx[closed_mask][i],
+                            y0=y_domain[0],
+                            x1=end_idx[closed_mask][i],
+                            y1=y_domain[1],
+                            fillcolor='teal',
+                            opacity=0.2,
+                            layer="below",
+                            line_width=0,
+                        ), closed_shape_kwargs))
+
+            open_mask = status == RangeStatus.Open
+            if open_mask.any():
+                # Plot end markers
+                open_end_customdata = np.stack((
+                    id_[open_mask],
+                    duration[open_mask]
+                ), axis=1)
+                open_end_scatter = go.Scatter(
+                    x=end_idx[open_mask],
+                    y=end_val[open_mask],
+                    mode='markers',
+                    marker=dict(
+                        symbol='diamond',
+                        color=plotting_cfg['contrast_color_schema']['orange'],
+                        size=7,
+                        line=dict(
+                            width=1,
+                            color=adjust_lightness(plotting_cfg['contrast_color_schema']['orange'])
+                        )
+                    ),
+                    name='Open',
+                    customdata=open_end_customdata,
+                    hovertemplate=f"{id_title}: %{{customdata[0]}}"
+                                  f"<br>{end_idx_title}: %{{x}}"
+                                  f"<br>Duration: %{{customdata[1]}}"
+                )
+                open_end_scatter.update(**end_trace_kwargs)
+                fig.add_trace(open_end_scatter, **add_trace_kwargs)
+
+                if plot_zones:
+                    # Plot open range zones
+                    for i in range(len(id_[open_mask])):
+                        fig.add_shape(**merge_dicts(dict(
+                            type="rect",
+                            xref=xref,
+                            yref="paper",
+                            x0=start_idx[open_mask][i],
+                            y0=y_domain[0],
+                            x1=end_idx[open_mask][i],
+                            y1=y_domain[1],
+                            fillcolor='orange',
+                            opacity=0.2,
+                            layer="below",
+                            line_width=0,
+                        ), open_shape_kwargs))
+
+        return fig
+
+
+Ranges.override_field_config_doc(__pdoc__)
+Ranges.override_metrics_doc(__pdoc__)
diff --git a/vectorbt/generic/stats_builder.py b/vectorbt/generic/stats_builder.py
index c88cc7bc..45408fe4 100644
--- a/vectorbt/generic/stats_builder.py
+++ b/vectorbt/generic/stats_builder.py
@@ -31,7 +31,7 @@ class StatsBuilderMixin(metaclass=MetaStatsBuilderMixin):
     Required to be a subclass of `vectorbt.base.array_wrapper.Wrapping`."""
 
     def __init__(self):
-        checks.assert_type(self, Wrapping)
+        checks.assert_instance_of(self, Wrapping)
 
         # Copy writeable attrs
         self._metrics = self.__class__._metrics.copy()
@@ -81,7 +81,7 @@ def stats_defaults(self) -> tp.Kwargs:
 
     @property
     def metrics(self) -> Config:
-        """Metrics supported by `StatsBuilderMixin.stats`.
+        """Metrics supported by `${cls_name}`.
 
         ```json
         ${metrics}
@@ -243,11 +243,11 @@ def stats(self,
         """
         # Resolve defaults
         if silence_warnings is None:
-            silence_warnings = self.stats_defaults['silence_warnings']
-        template_mapping = merge_dicts(self.stats_defaults['template_mapping'], template_mapping)
-        filters = merge_dicts(self.stats_defaults['filters'], filters)
-        settings = merge_dicts(self.stats_defaults['settings'], settings)
-        metric_settings = merge_dicts(self.stats_defaults['metric_settings'], metric_settings)
+            silence_warnings = self.stats_defaults.get('silence_warnings', False)
+        template_mapping = merge_dicts(self.stats_defaults.get('template_mapping', {}), template_mapping)
+        filters = merge_dicts(self.stats_defaults.get('filters', {}), filters)
+        settings = merge_dicts(self.stats_defaults.get('settings', {}), settings)
+        metric_settings = merge_dicts(self.stats_defaults.get('metric_settings', {}), metric_settings)
 
         # Replace templates globally (not used at metric level)
         if len(template_mapping) > 0:
@@ -264,7 +264,7 @@ def stats(self,
 
         # Prepare metrics
         if metrics is None:
-            metrics = reself.stats_defaults['metrics']
+            metrics = reself.stats_defaults.get('metrics', 'all')
         if metrics == 'all':
             metrics = reself.metrics
         if isinstance(metrics, dict):
@@ -274,7 +274,7 @@ def stats(self,
 
         # Prepare tags
         if tags is None:
-            tags = reself.stats_defaults['tags']
+            tags = reself.stats_defaults.get('tags', 'all')
         if isinstance(tags, str) and tags == 'all':
             tags = None
         if isinstance(tags, (str, tuple)):
@@ -488,38 +488,40 @@ def _getattr_func(obj: tp.Any,
                         if 'group_by' in passed_kwargs_out:
                             if 'pass_group_by' not in final_kwargs:
                                 final_kwargs.pop('group_by', None)
-                    if not callable(calc_func):
-                        raise TypeError("calc_func must be callable")
 
                     # Resolve arguments
-                    func_arg_names = get_func_arg_names(calc_func)
-                    for k in func_arg_names:
-                        if k not in final_kwargs:
-                            if final_kwargs.pop('resolve_' + k, False):
-                                try:
-                                    arg_out = custom_reself.resolve_attr(
-                                        k,
-                                        cond_kwargs=final_kwargs,
-                                        custom_arg_names=custom_arg_names,
-                                        cache_dct=arg_cache_dct,
-                                        use_caching=use_caching
-                                    )
-                                except AttributeError:
-                                    continue
-                                final_kwargs[k] = arg_out
-                    for k in list(final_kwargs.keys()):
-                        if k in opt_arg_names:
-                            if 'pass_' + k in final_kwargs:
-                                if not final_kwargs.get('pass_' + k):  # first priority
+                    if callable(calc_func):
+                        func_arg_names = get_func_arg_names(calc_func)
+                        for k in func_arg_names:
+                            if k not in final_kwargs:
+                                if final_kwargs.pop('resolve_' + k, False):
+                                    try:
+                                        arg_out = custom_reself.resolve_attr(
+                                            k,
+                                            cond_kwargs=final_kwargs,
+                                            custom_arg_names=custom_arg_names,
+                                            cache_dct=arg_cache_dct,
+                                            use_caching=use_caching
+                                        )
+                                    except AttributeError:
+                                        continue
+                                    final_kwargs[k] = arg_out
+                        for k in list(final_kwargs.keys()):
+                            if k in opt_arg_names:
+                                if 'pass_' + k in final_kwargs:
+                                    if not final_kwargs.get('pass_' + k):  # first priority
+                                        final_kwargs.pop(k, None)
+                                elif k not in func_arg_names:  # second priority
                                     final_kwargs.pop(k, None)
-                            elif k not in func_arg_names:  # second priority
-                                final_kwargs.pop(k, None)
-                    for k in list(final_kwargs.keys()):
-                        if k.startswith('pass_') or k.startswith('resolve_'):
-                            final_kwargs.pop(k, None)  # cleanup
-
-                    # Call calc_func
-                    out = calc_func(**final_kwargs)
+                        for k in list(final_kwargs.keys()):
+                            if k.startswith('pass_') or k.startswith('resolve_'):
+                                final_kwargs.pop(k, None)  # cleanup
+
+                        # Call calc_func
+                        out = calc_func(**final_kwargs)
+                    else:
+                        # calc_func is already a result
+                        out = calc_func
                 else:
                     # Do not resolve calc_func
                     out = calc_func(custom_reself, _metric_settings)
@@ -590,6 +592,8 @@ def _getattr_func(obj: tp.Any,
         stats_df = pd.DataFrame(stats_dct, index=new_index)
         return stats_df
 
+    # ############# Docs ############# #
+
     @classmethod
     def build_metrics_doc(cls, source_cls: tp.Optional[type] = None) -> str:
         """Build metrics documentation."""
@@ -598,7 +602,7 @@ def build_metrics_doc(cls, source_cls: tp.Optional[type] = None) -> str:
         return string.Template(
             inspect.cleandoc(get_dict_attr(source_cls, 'metrics').__doc__)
         ).substitute(
-            {'metrics': cls.metrics.to_doc()}
+            {'metrics': cls.metrics.to_doc(), 'cls_name': cls.__name__}
         )
 
     @classmethod
diff --git a/vectorbt/indicators/basic.py b/vectorbt/indicators/basic.py
index 54971fa7..0ed23fcf 100644
--- a/vectorbt/indicators/basic.py
+++ b/vectorbt/indicators/basic.py
@@ -54,7 +54,7 @@
 
 >>> ohlcv.vbt.ohlcv.plot()
 ```
-![](/vectorbt/docs/img/basic_price.svg)"""
+![](/docs/img/basic_price.svg)"""
 
 import numpy as np
 import plotly.graph_objects as go
@@ -119,7 +119,7 @@ def plot(self,
         >>> vbt.MA.run(ohlcv['Close'], 10).plot()
         ```
 
-        ![](/vectorbt/docs/img/MA.svg)
+        ![](/docs/img/MA.svg)
         """
         from vectorbt._settings import settings
         plotting_cfg = settings['plotting']
@@ -205,7 +205,7 @@ def plot(self,
         >>> vbt.MSTD.run(ohlcv['Close'], 10).plot()
         ```
 
-        ![](/vectorbt/docs/img/MSTD.svg)
+        ![](/docs/img/MSTD.svg)
         """
         self_col = self.select_one(column=column)
 
@@ -296,7 +296,7 @@ def plot(self,
         >>> vbt.BBANDS.run(ohlcv['Close']).plot()
         ```
 
-        ![](/vectorbt/docs/img/BBANDS.svg)
+        ![](/docs/img/BBANDS.svg)
         """
         from vectorbt._settings import settings
         plotting_cfg = settings['plotting']
@@ -414,7 +414,7 @@ def plot(self,
         >>> vbt.RSI.run(ohlcv['Close']).plot()
         ```
 
-        ![](/vectorbt/docs/img/RSI.svg)
+        ![](/docs/img/RSI.svg)
         """
         self_col = self.select_one(column=column)
 
@@ -517,7 +517,7 @@ def plot(self,
         >>> vbt.STOCH.run(ohlcv['High'], ohlcv['Low'], ohlcv['Close']).plot()
         ```
 
-        ![](/vectorbt/docs/img/STOCH.svg)
+        ![](/docs/img/STOCH.svg)
         """
         self_col = self.select_one(column=column)
 
@@ -630,7 +630,7 @@ def plot(self,
         >>> vbt.MACD.run(ohlcv['Close']).plot()
         ```
 
-        ![](/vectorbt/docs/img/MACD.svg)"""
+        ![](/docs/img/MACD.svg)"""
         self_col = self.select_one(column=column)
 
         if fig is None:
@@ -741,7 +741,7 @@ def plot(self,
         >>> vbt.ATR.run(ohlcv['High'], ohlcv['Low'], ohlcv['Close'], 10).plot()
         ```
 
-        ![](/vectorbt/docs/img/ATR.svg)
+        ![](/docs/img/ATR.svg)
         """
         self_col = self.select_one(column=column)
 
@@ -814,7 +814,7 @@ def plot(self,
         >>> vbt.OBV.run(ohlcv['Close'], ohlcv['Volume']).plot()
         ```
 
-        ![](/vectorbt/docs/img/OBV.svg)
+        ![](/docs/img/OBV.svg)
         """
         self_col = self.select_one(column=column)
 
diff --git a/vectorbt/indicators/factory.py b/vectorbt/indicators/factory.py
index 3ba0fd03..e1c191e2 100644
--- a/vectorbt/indicators/factory.py
+++ b/vectorbt/indicators/factory.py
@@ -979,7 +979,7 @@
 >>> MyIndExtended.run(price, [2, 3])[(2, 'a')].plot()
 ```
 
-![](/vectorbt/docs/img/MyInd_plot.svg)
+![](/docs/img/MyInd_plot.svg)
 
 ## Helper properties and methods
 
@@ -1974,9 +1974,12 @@ def _split_output(output):
            other_list
 
 
-def combine_objs(obj: tp.SeriesFrame, other: tp.MaybeTupleList[tp.Union[tp.ArrayLike, BaseAccessor]],
-                 *args, level_name: tp.Optional[str] = None, keys: tp.Optional[tp.IndexLike] = None,
-                 allow_multiple: bool = True, **kwargs) -> tp.SeriesFrame:
+def combine_objs(obj: tp.SeriesFrame,
+                 other: tp.MaybeTupleList[tp.Union[tp.ArrayLike, BaseAccessor]],
+                 *args, level_name: tp.Optional[str] = None,
+                 keys: tp.Optional[tp.IndexLike] = None,
+                 allow_multiple: bool = True,
+                 **kwargs) -> tp.SeriesFrame:
     """Combines/compares `obj` to `other`, for example, to generate signals.
 
     Both will broadcast together.
@@ -2076,7 +2079,7 @@ def __init__(self,
             checks.assert_len_equal(param_list[0], params)
         for mapper in mapper_list:
             checks.assert_equal(len(mapper), wrapper.shape_2d[1])
-        checks.assert_type(short_name, str)
+        checks.assert_instance_of(short_name, str)
         checks.assert_len_equal(level_names, param_list)
 
         setattr(self, '_short_name', short_name)
@@ -2225,14 +2228,14 @@ def __init__(self,
         """
         # Check and save parameters
         self.class_name = class_name
-        checks.assert_type(class_name, str)
+        checks.assert_instance_of(class_name, str)
 
         self.class_docstring = class_docstring
-        checks.assert_type(class_docstring, str)
+        checks.assert_instance_of(class_docstring, str)
 
         self.module_name = module_name
         if module_name is not None:
-            checks.assert_type(module_name, str)
+            checks.assert_instance_of(module_name, str)
 
         if short_name is None:
             if class_name == 'Indicator':
@@ -2240,10 +2243,10 @@ def __init__(self,
             else:
                 short_name = class_name.lower()
         self.short_name = short_name
-        checks.assert_type(short_name, str)
+        checks.assert_instance_of(short_name, str)
 
         self.prepend_name = prepend_name
-        checks.assert_type(prepend_name, bool)
+        checks.assert_instance_of(prepend_name, bool)
 
         if input_names is None:
             input_names = []
@@ -2279,19 +2282,19 @@ def __init__(self,
 
         if output_flags is None:
             output_flags = {}
-        checks.assert_type(output_flags, dict)
+        checks.assert_instance_of(output_flags, dict)
         if len(output_flags) > 0:
             checks.assert_dict_valid(output_flags, all_output_names)
         self.output_flags = output_flags
 
         if custom_output_props is None:
             custom_output_props = {}
-        checks.assert_type(custom_output_props, dict)
+        checks.assert_instance_of(custom_output_props, dict)
         self.custom_output_props = custom_output_props
 
         if attr_settings is None:
             attr_settings = {}
-        checks.assert_type(attr_settings, dict)
+        checks.assert_instance_of(attr_settings, dict)
         all_attr_names = input_names + all_output_names + list(custom_output_props.keys())
         if len(attr_settings) > 0:
             checks.assert_dict_valid(attr_settings, all_attr_names)
@@ -3460,8 +3463,8 @@ def parse_pandas_ta_config(cls, func: tp.Callable, test_input_names: tp.Optional
         new_args = {c: test_df[c] for c in input_names}
         try:
             result = func(**new_args)
-        except:
-            raise ValueError("Couldn't parse the indicator")
+        except Exception as e:
+            raise ValueError("Couldn't parse the indicator: " + str(e))
 
         # Concatenate Series/DataFrames if the result is a tuple
         if isinstance(result, tuple):
@@ -3528,7 +3531,7 @@ def get_pandas_ta_indicators(cls, silence_warnings: bool = True) -> tp.Set[str]:
                 indicators.add(func_name.upper())
             except Exception as e:
                 if not silence_warnings:
-                    warnings.warn(str(e), stacklevel=2)
+                    warnings.warn(f"Function {func_name}: " + str(e), stacklevel=2)
         return indicators
 
     @classmethod
diff --git a/vectorbt/labels/generators.py b/vectorbt/labels/generators.py
index 7a1d0d79..afe463c3 100644
--- a/vectorbt/labels/generators.py
+++ b/vectorbt/labels/generators.py
@@ -75,12 +75,13 @@
 # ############# Label generators ############# #
 
 
-def _plot(self, **kwargs) -> tp.BaseFigure:  # pragma: no cover
-    """Plot `close` and overlay it with the heatmap of `labels`."""
-    if self.wrapper.ndim > 1:
-        raise TypeError("Select a column first. Use indexing.")
+def _plot(self, column: tp.Optional[tp.Label] = None, **kwargs) -> tp.BaseFigure:  # pragma: no cover
+    """Plot `close` and overlay it with the heatmap of `labels`.
 
-    return self.close.rename('close').vbt.overlay_with_heatmap(self.labels.rename('labels'), **kwargs)
+    `**kwargs` are passed to `vectorbt.generic.accessors.GenericSRAccessor.overlay_with_heatmap`."""
+    self_col = self.select_one(column=column, group_by=False)
+
+    return self_col.close.rename('close').vbt.overlay_with_heatmap(self_col.labels.rename('labels'), **kwargs)
 
 
 FIXLB = IndicatorFactory(
diff --git a/vectorbt/ohlcv_accessors.py b/vectorbt/ohlcv_accessors.py
index fe283643..6a7ed80a 100644
--- a/vectorbt/ohlcv_accessors.py
+++ b/vectorbt/ohlcv_accessors.py
@@ -69,7 +69,7 @@ def plot(self,
         >>> vbt.YFData.download("BTC-USD").get().vbt.ohlcv.plot()
         ```
 
-        ![](/vectorbt/docs/img/ohlcv.svg)
+        ![](/docs/img/ohlcv.svg)
         """
         from vectorbt._settings import settings
         ohlcv_cfg = settings['ohlcv']
diff --git a/vectorbt/portfolio/base.py b/vectorbt/portfolio/base.py
index 39537cb2..ef15e5fb 100644
--- a/vectorbt/portfolio/base.py
+++ b/vectorbt/portfolio/base.py
@@ -91,7 +91,7 @@
 >>> pf.value().vbt.plot()
 ```
 
-![](/vectorbt/docs/img/portfolio_value.svg)
+![](/docs/img/portfolio_value.svg)
 
 ## Broadcasting
 
@@ -380,7 +380,7 @@
 Total Trades                                         10
 Total Closed Trades                                  10
 Total Open Trades                                     0
-Open Trade P&L                                      0.0
+Open Trade PnL                                      0.0
 Win Rate [%]                                       60.0
 Best Trade [%]                                 15.31962
 Worst Trade [%]                               -9.904223
@@ -420,7 +420,7 @@
 Total Trades                                         10
 Total Closed Trades                                  10
 Total Open Trades                                     0
-Open Trade P&L                                      0.0
+Open Trade PnL                                      0.0
 Win Rate [%]                                       60.0
 Best Trade [%]                                 15.31962
 Worst Trade [%]                               -9.904223
@@ -462,7 +462,7 @@
 Total Trades                                         30
 Total Closed Trades                                  30
 Total Open Trades                                     0
-Open Trade P&L                                      0.0
+Open Trade PnL                                      0.0
 Win Rate [%]                                  66.666667
 Best Trade [%]                                18.332559
 Worst Trade [%]                               -9.904223
@@ -506,7 +506,7 @@
 Total Trades                                       15.0
 Total Closed Trades                                15.0
 Total Open Trades                                   0.0
-Open Trade P&L                                      0.0
+Open Trade PnL                                      0.0
 Win Rate [%]                                       65.0
 Best Trade [%]                                 16.82609
 Worst Trade [%]                               -9.701273
@@ -554,7 +554,7 @@
 >>> pf.stats(column=10, tags=['trades'])
 Total Trades                                10
 Total Open Trades                            0
-Open Trade P&L                               0
+Open Trade PnL                               0
 Long Trades [%]                            100
 Win Rate [%]                                60
 Best Trade [%]                         15.3196
@@ -573,7 +573,7 @@
 ```python-repl
 >>> pf.stats(column=10, tags='trades and open and not closed')
 Total Open Trades    0.0
-Open Trade P&L       0.0
+Open Trade PnL       0.0
 Name: 10, dtype: float64
 ```
 
@@ -602,7 +602,7 @@
 Total Trades                                         10
 Total Closed Trades                                  10
 Total Open Trades                                     0
-Open Trade P&L                                      0.0
+Open Trade PnL                                      0.0
 Win Rate [%]                                       60.0
 Best Trade [%]                                 15.31962
 Worst Trade [%]                               -9.904223
@@ -684,14 +684,32 @@
 (a similar resolution procedure), and calling the method/property. The result of the resolution
 process is then passed as `arg` (or `trades` in our example).
 
-Here's an example without argument resolution:
+Here's an example without resolution of arguments:
 
 ```python-repl
 >>> max_winning_streak = (
 ...     'max_winning_streak',
 ...     dict(
 ...         title='Max Winning Streak',
-...         calc_func=lambda self, group_by: self.get_trades(group_by=group_by).winning_streak.max()
+...         calc_func=lambda self, group_by:
+...         self.get_trades(group_by=group_by).winning_streak.max()
+...     )
+... )
+>>> pf.stats(metrics=max_winning_streak, column=10)
+Max Winning Streak    3.0
+Name: 10, dtype: float64
+```
+
+And here's an example without resolution of the calculation function:
+
+```python-repl
+>>> max_winning_streak = (
+...     'max_winning_streak',
+...     dict(
+...         title='Max Winning Streak',
+...         calc_func=lambda self, settings:
+...         self.get_trades(group_by=settings['group_by']).winning_streak.max(),
+...         resolve_calc_func=False
 ...     )
 ... )
 >>> pf.stats(metrics=max_winning_streak, column=10)
@@ -735,7 +753,7 @@
 Total Positions                                         10
 Total Closed Positions                                  10
 Total Open Positions                                     0
-Open Position P&L                                      0.0
+Open Position PnL                                      0.0
 Win Rate [%]                                          60.0
 Best Position [%]                                 15.31962
 Worst Position [%]                               -9.904223
@@ -781,7 +799,7 @@
 Total Trades                                       15.0
 Total Closed Trades                                15.0
 Total Open Trades                                   0.0
-Open Trade P&L                                      0.0
+Open Trade PnL                                      0.0
 Win Rate [%]                                       65.0
 Best Trade [%]                                 16.82609
 Worst Trade [%]                               -9.701273
@@ -827,28 +845,28 @@
 Name: 10, dtype: int64
 ```
 
-Here's an example of a parametrized metric. Let's get the number of trades with P&L over some amount:
+Here's an example of a parametrized metric. Let's get the number of trades with PnL over some amount:
 
 ```python-repl
 >>> trade_min_pnl_cnt = (
 ...     'trade_min_pnl_cnt',
 ...     dict(
-...         title=vbt.Sub('Trades with P&L over $$${min_pnl}'),
-...         calc_func=lambda trades, min_pnl: trades.filter_by_mask(
-...             trades.pnl.values >= min_pnl).count()
-...         resolve_trades=True,
+...         title=vbt.Sub('Trades with PnL over $$${min_pnl}'),
+...         calc_func=lambda trades, min_pnl: trades.apply_mask(
+...             trades.pnl.values >= min_pnl).count(),
+...         resolve_trades=True
 ...     )
 ... )
 >>> pf.stats(
 ...     metrics=trade_min_pnl_cnt, column=10,
 ...     metric_settings=dict(trade_min_pnl_cnt=dict(min_pnl=0)))
-Trades with P&L over $0    6
+Trades with PnL over $0    6
 Name: stats, dtype: int64
 
 >>> pf.stats(
 ...     metrics=trade_min_pnl_cnt, column=10,
 ...     metric_settings=dict(trade_min_pnl_cnt=dict(min_pnl=10)))
-Trades with P&L over $10    1
+Trades with PnL over $10    1
 Name: stats, dtype: int64
 ```
 
@@ -868,9 +886,9 @@
 ...         trade_min_pnl_cnt_1=dict(min_pnl=10),
 ...         trade_min_pnl_cnt_2=dict(min_pnl=20))
 ...     )
-Trades with P&L over $0     6
-Trades with P&L over $10    1
-Trades with P&L over $20    0
+Trades with PnL over $0     6
+Trades with PnL over $10    1
+Trades with PnL over $20    0
 Name: stats, dtype: int64
 ```
 
@@ -895,7 +913,7 @@
 Total Trades                                         10
 Total Closed Trades                                  10
 Total Open Trades                                     0
-Open Trade P&L                                      0.0
+Open Trade PnL                                      0.0
 Win Rate [%]                                       60.0
 Best Trade [%]                                 15.31962
 Worst Trade [%]                               -9.904223
@@ -948,7 +966,7 @@
 >>> pf.plot(column=10)
 ```
 
-![](/vectorbt/docs/img/portfolio_plot.svg)
+![](/docs/img/portfolio_plot.svg)
 
 You can choose any of the subplots in `Portfolio.subplots`, in any order, and
 control their appearance using keyword arguments:
@@ -971,7 +989,7 @@
 ... )
 ```
 
-![](/vectorbt/docs/img/portfolio_plot_drawdowns.svg)
+![](/docs/img/portfolio_plot_drawdowns.svg)
 
 To create a new subplot, a preferred way is to pass a plotting function:
 
@@ -1017,7 +1035,7 @@
 ... )
 ```
 
-![](/vectorbt/docs/img/portfolio_plot_custom.svg)
+![](/docs/img/portfolio_plot_custom.svg)
 
 If a plotting function can in any way be accessed from the current portfolio, you can pass
 the path to this function (see `vectorbt.utils.attr.deep_getattr` for the path format).
@@ -1064,7 +1082,7 @@
 >>> pf.plot(subplots, column=10, template_mapping=dict(window=10))
 ```
 
-![](/vectorbt/docs/img/portfolio_plot_path.svg)
+![](/docs/img/portfolio_plot_path.svg)
 """
 import numpy as np
 import pandas as pd
@@ -1091,7 +1109,7 @@
 from vectorbt.portfolio.trades import Trades, Positions
 from vectorbt.portfolio.logs import Logs
 from vectorbt.portfolio.enums import *
-from vectorbt.portfolio.decorators import add_returns_acc_methods
+from vectorbt.portfolio.decorators import attach_returns_acc_methods
 
 __pdoc__ = {}
 
@@ -1120,9 +1138,8 @@
         'drawdown': dict(),
         'max_drawdown': dict()
     },
-    as_attrs=False,
     readonly=True,
-    copy_kwargs=dict(copy_mode='deep')
+    as_attrs=False
 )
 """_"""
 
@@ -1140,7 +1157,7 @@ class MetaPortfolio(type(StatsBuilderMixin), type(PlotBuilderMixin)):
     pass
 
 
-@add_returns_acc_methods(returns_acc_config)
+@attach_returns_acc_methods(returns_acc_config)
 class Portfolio(Wrapping, StatsBuilderMixin, PlotBuilderMixin, metaclass=MetaPortfolio):
     """Class for modeling portfolio and measuring its performance.
 
@@ -2045,7 +2062,7 @@ def from_orders(cls: tp.Type[PortfolioT],
             close (array_like): Last asset price at each time step.
                 Will broadcast.
 
-                Used for calculating unrealized P&L and portfolio value.
+                Used for calculating unrealized PnL and portfolio value.
             size (float or array_like): Size to order.
                 See `vectorbt.portfolio.enums.Order.size`. Will broadcast.
             size_type (SizeType or array_like): See `vectorbt.portfolio.enums.SizeType`.
@@ -2270,7 +2287,7 @@ def from_orders(cls: tp.Type[PortfolioT],
         >>> pf.asset_value(group_by=False).vbt.plot()
         ```
 
-        ![](/vectorbt/docs/img/simulate_nb.svg)
+        ![](/docs/img/simulate_nb.svg)
         """
         # Get defaults
         from vectorbt._settings import settings
@@ -2475,7 +2492,7 @@ def from_order_func(cls: tp.Type[PortfolioT],
             close (array_like): Last asset price at each time step.
                 Will broadcast to `target_shape`.
 
-                Used for calculating unrealized P&L and portfolio value.
+                Used for calculating unrealized PnL and portfolio value.
             order_func_nb (callable): Order generation function.
             *order_args: Arguments passed to `order_func_nb`.
             target_shape (tuple): Target shape to iterate over. Defaults to `close.shape`.
@@ -2728,7 +2745,7 @@ def from_order_func(cls: tp.Type[PortfolioT],
         >>> pf.asset_value(group_by=False).vbt.plot()
         ```
 
-        ![](/vectorbt/docs/img/simulate_nb.svg)
+        ![](/docs/img/simulate_nb.svg)
 
         Combine multiple exit conditions. Exit early if the price hits some threshold before an actual exit
         (similar to the example under `Portfolio.from_signals`, but doesn't remove any information):
@@ -3073,14 +3090,14 @@ def order_records(self) -> tp.RecordArray:
     @cached_property
     def orders(self) -> Orders:
         """`Portfolio.get_orders` with default arguments."""
-        return Orders(self.wrapper, self.order_records, self.close)
+        return self.get_orders()
 
     @cached_method
-    def get_orders(self, group_by: tp.GroupByLike = None) -> Orders:
+    def get_orders(self, group_by: tp.GroupByLike = None, **kwargs) -> Orders:
         """Get order records.
 
         See `vectorbt.portfolio.orders.Orders`."""
-        return self.orders.regroup(group_by)
+        return Orders(self.wrapper, self.order_records, close=self.close, **kwargs).regroup(group_by)
 
     @property
     def log_records(self) -> tp.RecordArray:
@@ -3090,38 +3107,38 @@ def log_records(self) -> tp.RecordArray:
     @cached_property
     def logs(self) -> Logs:
         """`Portfolio.get_logs` with default arguments."""
-        return Logs(self.wrapper, self.log_records)
+        return self.get_logs()
 
     @cached_method
-    def get_logs(self, group_by: tp.GroupByLike = None) -> Logs:
+    def get_logs(self, group_by: tp.GroupByLike = None, **kwargs) -> Logs:
         """Get log records.
 
         See `vectorbt.portfolio.logs.Logs`."""
-        return self.logs.regroup(group_by)
+        return Logs(self.wrapper, self.log_records, **kwargs).regroup(group_by)
 
     @cached_property
     def trades(self) -> Trades:
         """`Portfolio.get_trades` with default arguments."""
-        return Trades.from_orders(self.orders)
+        return self.get_trades()
 
     @cached_method
-    def get_trades(self, group_by: tp.GroupByLike = None) -> Trades:
+    def get_trades(self, group_by: tp.GroupByLike = None, **kwargs) -> Trades:
         """Get trade records.
 
         See `vectorbt.portfolio.trades.Trades`."""
-        return self.trades.regroup(group_by)
+        return Trades.from_orders(self.orders, **kwargs).regroup(group_by)
 
     @cached_property
     def positions(self) -> Positions:
         """`Portfolio.get_positions` with default arguments."""
-        return Positions.from_trades(self.trades)
+        return self.get_positions()
 
     @cached_method
-    def get_positions(self, group_by: tp.GroupByLike = None) -> Positions:
+    def get_positions(self, group_by: tp.GroupByLike = None, **kwargs) -> Positions:
         """Get position records.
 
         See `vectorbt.portfolio.trades.Positions`."""
-        return self.positions.regroup(group_by)
+        return Positions.from_trades(self.trades, **kwargs).regroup(group_by)
 
     @cached_property
     def drawdowns(self) -> Drawdowns:
@@ -3129,13 +3146,14 @@ def drawdowns(self) -> Drawdowns:
         return self.get_drawdowns()
 
     @cached_method
-    def get_drawdowns(self, group_by: tp.GroupByLike = None, **kwargs) -> Drawdowns:
+    def get_drawdowns(self, group_by: tp.GroupByLike = None, wrap_kwargs: tp.KwargsLike = None,
+                      wrapper_kwargs: tp.KwargsLike = None, **kwargs) -> Drawdowns:
         """Get drawdown records from `Portfolio.value`.
 
-        See `vectorbt.generic.drawdowns.Drawdowns`.
-
-        `**kwargs` are passed to `Portfolio.value`."""
-        return Drawdowns.from_ts(self.value(group_by=group_by, **kwargs), freq=self.wrapper.freq)
+        See `vectorbt.generic.drawdowns.Drawdowns`."""
+        wrapper_kwargs = merge_dicts(self.orders.wrapper.config, wrapper_kwargs, dict(group_by=None))
+        value = self.value(group_by=group_by, wrap_kwargs=wrap_kwargs)
+        return Drawdowns.from_ts(value, wrapper_kwargs=wrapper_kwargs, **kwargs)
 
     # ############# Assets ############# #
 
@@ -3621,7 +3639,7 @@ def stats_defaults(self) -> tp.Kwargs:
                 tags=['portfolio', Rep("trades_tag"), 'open']
             ),
             open_trade_pnl=dict(
-                title=RepEval("'Open Position P&L' if use_positions else 'Open Trade P&L'"),
+                title=RepEval("'Open Position PnL' if use_positions else 'Open Trade PnL'"),
                 calc_func='trades.open.pnl.sum',
                 incl_open=True,
                 tags=['portfolio', Rep("trades_tag"), 'open']
@@ -3742,6 +3760,31 @@ def returns_stats(self,
 
     # ############# Plotting ############# #
 
+    def plot_orders(self, column: tp.Optional[tp.Label] = None, **kwargs) -> tp.BaseFigure:
+        """Plot one column/group of orders."""
+        kwargs = merge_dicts(dict(close_trace_kwargs=dict(name='Close')), kwargs)
+        return self.orders.regroup(False).plot(column=column, **kwargs)
+
+    def plot_trades(self, column: tp.Optional[tp.Label] = None, **kwargs) -> tp.BaseFigure:
+        """Plot one column/group of trades."""
+        kwargs = merge_dicts(dict(close_trace_kwargs=dict(name='Close')), kwargs)
+        return self.trades.regroup(False).plot(column=column, **kwargs)
+
+    def plot_trade_pnl(self, column: tp.Optional[tp.Label] = None, **kwargs) -> tp.BaseFigure:
+        """Plot one column/group of trade PnL."""
+        kwargs = merge_dicts(dict(close_trace_kwargs=dict(name='Close')), kwargs)
+        return self.trades.regroup(False).plot_pnl(column=column, **kwargs)
+
+    def plot_positions(self, column: tp.Optional[tp.Label] = None, **kwargs) -> tp.BaseFigure:
+        """Plot one column/group of positions."""
+        kwargs = merge_dicts(dict(close_trace_kwargs=dict(name='Close')), kwargs)
+        return self.positions.regroup(False).plot(column=column, **kwargs)
+
+    def plot_position_pnl(self, column: tp.Optional[tp.Label] = None, **kwargs) -> tp.BaseFigure:
+        """Plot one column/group of position PnL."""
+        kwargs = merge_dicts(dict(close_trace_kwargs=dict(name='Close')), kwargs)
+        return self.positions.regroup(False).plot_pnl(column=column, **kwargs)
+
     def plot_asset_flow(self,
                         column: tp.Optional[tp.Label] = None,
                         direction: str = 'all',
@@ -4323,35 +4366,32 @@ def plot_defaults(self) -> tp.Kwargs:
                 tags=['portfolio', 'orders']
             ),
             trades=dict(
-                title=RepEval("'Positions' if use_positions else 'Trades'"),
+                title="Trades",
                 yaxis_title="Price",
                 check_is_not_grouped=True,
                 plot_func='trades.plot',
-                tags=RepEval("['portfolio', trades_tag, *incl_open_tags]")
+                tags=RepEval("['portfolio', 'trades', *incl_open_tags]")
             ),
             trade_pnl=dict(
-                title=RepEval("'Position P&L' if use_positions else 'Trade P&L'"),
-                yaxis_title=RepEval("'Position P&L' if use_positions else 'Trade P&L'"),
+                title="Trade PnL",
+                yaxis_title="Trade PnL",
                 check_is_not_grouped=True,
                 plot_func='trades.plot_pnl',
-                pass_column=True,
-                pass_hline_shape_kwargs=True,
-                pass_add_trace_kwargs=True,
-                pass_xref=True,
-                pass_yref=True,
-                tags=RepEval("['portfolio', trades_tag, *incl_open_tags]")
+                tags=RepEval("['portfolio', 'trades', *incl_open_tags]")
             ),
-            trade_returns=dict(
-                title=RepEval("'Position Returns' if use_positions else 'Trade Returns'"),
-                yaxis_title=RepEval("'Position returns' if use_positions else 'Trade returns'"),
+            positions=dict(
+                title="Positions",
+                yaxis_title="Price",
                 check_is_not_grouped=True,
-                plot_func='trades.plot_returns',
-                pass_column=True,
-                pass_hline_shape_kwargs=True,
-                pass_add_trace_kwargs=True,
-                pass_xref=True,
-                pass_yref=True,
-                tags=RepEval("['portfolio', trades_tag, *incl_open_tags]")
+                plot_func='positions.plot',
+                tags=RepEval("['portfolio', 'positions', *incl_open_tags]")
+            ),
+            position_pnl=dict(
+                title="Position PnL",
+                yaxis_title="Position PnL",
+                check_is_not_grouped=True,
+                plot_func='positions.plot_pnl',
+                tags=RepEval("['portfolio', 'positions', *incl_open_tags]")
             ),
             asset_flow=dict(
                 title="Asset Flow",
diff --git a/vectorbt/portfolio/decorators.py b/vectorbt/portfolio/decorators.py
index ce893844..211c43b7 100644
--- a/vectorbt/portfolio/decorators.py
+++ b/vectorbt/portfolio/decorators.py
@@ -8,7 +8,7 @@
 WrapperFuncT = tp.Callable[[tp.Type[tp.T]], tp.Type[tp.T]]
 
 
-def add_returns_acc_methods(config: Config) -> WrapperFuncT:
+def attach_returns_acc_methods(config: Config) -> WrapperFuncT:
     """Class decorator to add returns accessor methods.
 
     `config` should contain target method names (keys) and dictionaries (values) with the following keys:
@@ -20,7 +20,7 @@ def add_returns_acc_methods(config: Config) -> WrapperFuncT:
     """
 
     def wrapper(cls: tp.Type[tp.T]) -> tp.Type[tp.T]:
-        checks.assert_subclass(cls, "Portfolio")
+        checks.assert_subclass_of(cls, "Portfolio")
 
         for target_name, settings in config.items():
             source_name = settings.get('source_name', target_name)
diff --git a/vectorbt/portfolio/enums.py b/vectorbt/portfolio/enums.py
index 36688d89..32bdd771 100644
--- a/vectorbt/portfolio/enums.py
+++ b/vectorbt/portfolio/enums.py
@@ -20,7 +20,7 @@
     'Direction',
     'OrderStatus',
     'OrderSide',
-    'StatusInfo',
+    'OrderStatusInfo',
     'TradeDirection',
     'TradeStatus',
     'TradeType',
@@ -349,7 +349,7 @@ class OrderSideT(tp.NamedTuple):
 """
 
 
-class StatusInfoT(tp.NamedTuple):
+class OrderStatusInfoT(tp.NamedTuple):
     SizeNaN: int = 0
     PriceNaN: int = 1
     ValPriceNaN: int = 2
@@ -366,13 +366,13 @@ class StatusInfoT(tp.NamedTuple):
     PartialFill: int = 13
 
 
-StatusInfo = StatusInfoT()
+OrderStatusInfo = OrderStatusInfoT()
 """_"""
 
-__pdoc__['StatusInfo'] = f"""Order status information.
+__pdoc__['OrderStatusInfo'] = f"""Order status information.
 
 ```json
-{to_doc(StatusInfo)}
+{to_doc(OrderStatusInfo)}
 ```
 """
 
@@ -1200,6 +1200,8 @@ class Order(tp.NamedTuple):
 __pdoc__['Order.fixed_fees'] = "Fixed amount of fees to pay for this order."
 __pdoc__['Order.slippage'] = """Slippage in percentage of `Order.price`. 
 
+Slippage is a penalty applied on the price.
+
 Note that 0.01 = 1%."""
 __pdoc__['Order.min_size'] = """Minimum size in both directions. 
 
@@ -1262,7 +1264,7 @@ class OrderResult(tp.NamedTuple):
 __pdoc__['OrderResult.fees'] = "Total fees paid for this order."
 __pdoc__['OrderResult.side'] = "See `OrderSide`."
 __pdoc__['OrderResult.status'] = "See `OrderStatus`."
-__pdoc__['OrderResult.status_info'] = "See `StatusInfo`."
+__pdoc__['OrderResult.status_info'] = "See `OrderStatusInfo`."
 
 
 class AdjustSLContext(tp.NamedTuple):
@@ -1396,20 +1398,20 @@ class AdjustTPContext(tp.NamedTuple):
     ('free_cash', np.float_),
     ('val_price', np.float_),
     ('value', np.float_),
-    ('size', np.float_),
-    ('price', np.float_),
-    ('size_type', np.int_),
-    ('direction', np.int_),
-    ('fees', np.float_),
-    ('fixed_fees', np.float_),
-    ('slippage', np.float_),
-    ('min_size', np.float_),
-    ('max_size', np.float_),
-    ('reject_prob', np.float_),
-    ('lock_cash', np.bool_),
-    ('allow_partial', np.bool_),
-    ('raise_reject', np.bool_),
-    ('log', np.bool_),
+    ('req_size', np.float_),
+    ('req_price', np.float_),
+    ('req_size_type', np.int_),
+    ('req_direction', np.int_),
+    ('req_fees', np.float_),
+    ('req_fixed_fees', np.float_),
+    ('req_slippage', np.float_),
+    ('req_min_size', np.float_),
+    ('req_max_size', np.float_),
+    ('req_reject_prob', np.float_),
+    ('req_lock_cash', np.bool_),
+    ('req_allow_partial', np.bool_),
+    ('req_raise_reject', np.bool_),
+    ('req_log', np.bool_),
     ('new_cash', np.float_),
     ('new_position', np.float_),
     ('new_debt', np.float_),
diff --git a/vectorbt/portfolio/logs.py b/vectorbt/portfolio/logs.py
index 77254d6a..f668bdc4 100644
--- a/vectorbt/portfolio/logs.py
+++ b/vectorbt/portfolio/logs.py
@@ -1,13 +1,8 @@
 """Base class for working with log records.
 
-Class `Logs` wraps log records to analyze logs. Logs are mainly populated when
+Order records capture information on simulation logs. Logs are populated when
 simulating a portfolio and can be accessed as `vectorbt.portfolio.base.Portfolio.logs`.
 
-## Stats
-
-!!! hint
-    See `vectorbt.generic.stats_builder.StatsBuilderMixin.stats` and `Logs.metrics`.
-
 ```python-repl
 >>> import pandas as pd
 >>> import numpy as np
@@ -24,8 +19,31 @@
 ...     'b': np.random.uniform(-100, 100, size=100),
 ... }, index=[datetime(2020, 1, 1) + timedelta(days=i) for i in range(100)])
 >>> pf = vbt.Portfolio.from_orders(price, size, fees=0.01, freq='d', log=True)
+>>> logs = pf.logs
+
+>>> logs.filled.count()
+a    88
+b    99
+Name: count, dtype: int64
+
+>>> logs.ignored.count()
+a    0
+b    0
+Name: count, dtype: int64
+
+>>> logs.rejected.count()
+a    12
+b     1
+Name: count, dtype: int64
+```
 
->>> pf.logs.stats(column='a')
+## Stats
+
+!!! hint
+    See `vectorbt.generic.stats_builder.StatsBuilderMixin.stats` and `Logs.metrics`.
+
+```python-repl
+>>> logs['a'].stats()
 Start                             2020-01-01 00:00:00
 End                               2020-04-09 00:00:00
 Period                              100 days 00:00:00
@@ -42,7 +60,7 @@
 `Logs.stats` also supports (re-)grouping:
 
 ```python-repl
->>> pf.logs.stats(group_by=True)
+>>> logs.stats(group_by=True)
 Start                             2020-01-01 00:00:00
 End                               2020-04-09 00:00:00
 Period                              100 days 00:00:00
@@ -60,128 +78,190 @@
 
 from vectorbt import _typing as tp
 from vectorbt.utils.config import merge_dicts, Config
-from vectorbt.utils.enum import map_enum_values
-from vectorbt.base.array_wrapper import ArrayWrapper
 from vectorbt.base.reshape_fns import to_dict
 from vectorbt.generic.stats_builder import StatsBuilderMixin
 from vectorbt.records.base import Records
-from vectorbt.records.decorators import add_mapped_fields
+from vectorbt.records.decorators import attach_fields, override_field_config
 from vectorbt.portfolio.enums import (
     log_dt,
     SizeType,
     Direction,
     OrderSide,
     OrderStatus,
-    StatusInfo
+    OrderStatusInfo
 )
 
 __pdoc__ = {}
 
-logs_mf_config = Config(
+logs_field_config = Config(
+    dict(
+        dtype=log_dt,
+        settings=dict(
+            id=dict(
+                title='Log Id'
+            ),
+            group=dict(
+                title='Group'
+            ),
+            cash=dict(
+                title='Cash'
+            ),
+            position=dict(
+                title='Position'
+            ),
+            debt=dict(
+                title='Debt'
+            ),
+            free_cash=dict(
+                title='Free Cash'
+            ),
+            val_price=dict(
+                title='Val Price'
+            ),
+            value=dict(
+                title='Value'
+            ),
+            req_size=dict(
+                title='Request Size'
+            ),
+            req_price=dict(
+                title='Request Price'
+            ),
+            req_size_type=dict(
+                title='Request Size Type',
+                mapping=SizeType
+            ),
+            req_direction=dict(
+                title='Request Direction',
+                mapping=Direction
+            ),
+            req_fees=dict(
+                title='Request Fees'
+            ),
+            req_fixed_fees=dict(
+                title='Request Fixed Fees'
+            ),
+            req_slippage=dict(
+                title='Request Slippage'
+            ),
+            req_min_size=dict(
+                title='Request Min Size'
+            ),
+            req_max_size=dict(
+                title='Request Max Size'
+            ),
+            req_reject_prob=dict(
+                title='Request Rejection Prob'
+            ),
+            req_lock_cash=dict(
+                title='Request Lock Cash'
+            ),
+            req_allow_partial=dict(
+                title='Request Allow Partial'
+            ),
+            req_raise_reject=dict(
+                title='Request Raise Rejection'
+            ),
+            req_log=dict(
+                title='Request Log'
+            ),
+            new_cash=dict(
+                title='New Cash'
+            ),
+            new_position=dict(
+                title='New Position'
+            ),
+            new_debt=dict(
+                title='New Debt'
+            ),
+            new_free_cash=dict(
+                title='New Free Cash'
+            ),
+            new_val_price=dict(
+                title='New Val Price'
+            ),
+            new_value=dict(
+                title='New Value'
+            ),
+            res_size=dict(
+                title='Result Size'
+            ),
+            res_price=dict(
+                title='Result Price'
+            ),
+            res_fees=dict(
+                title='Result Fees'
+            ),
+            res_side=dict(
+                title='Result Side',
+                mapping=OrderSide
+            ),
+            res_status=dict(
+                title='Result Status',
+                mapping=OrderStatus
+            ),
+            res_status_info=dict(
+                title='Result Status Info',
+                mapping=OrderStatusInfo
+            ),
+            order_id=dict(
+                title='Order Id'
+            )
+        )
+    ),
+    readonly=True,
+    as_attrs=False
+)
+"""_"""
+
+__pdoc__['logs_field_config'] = f"""Field config for `Logs`.
+
+```json
+{logs_field_config.to_doc()}
+```
+"""
+
+logs_attach_field_config = Config(
     dict(
-        size_type=dict(defaults=dict(mapping=SizeType)),
-        direction=dict(defaults=dict(mapping=Direction)),
-        res_side=dict(defaults=dict(mapping=OrderSide)),
-        res_status=dict(defaults=dict(mapping=OrderStatus)),
-        res_status_info=dict(defaults=dict(mapping=StatusInfo))
+        res_side=dict(
+            attach_filters=True
+        ),
+        res_status=dict(
+            attach_filters=True
+        ),
+        res_status_info=dict(
+            attach_filters=True
+        )
     ),
-    as_attrs=False,
     readonly=True,
-    copy_kwargs=dict(copy_mode='deep')
+    as_attrs=False
 )
 """_"""
 
-__pdoc__['logs_mf_config'] = f"""Config of `vectorbt.portfolio.enums.log_dt` 
-mapped fields to be overridden in `Logs`.
+__pdoc__['logs_attach_field_config'] = f"""Config of fields to be attached to `Logs`.
 
 ```json
-{logs_mf_config.to_doc()}
+{logs_attach_field_config.to_doc()}
 ```
 """
 
+LogsT = tp.TypeVar("LogsT", bound="Logs")
+
 
-@add_mapped_fields(log_dt, logs_mf_config)
+@attach_fields(logs_attach_field_config)
+@override_field_config(logs_field_config)
 class Logs(Records):
-    """Extends `Records` for working with log records.
-
-    !!! note
-        Some features require the log records to be sorted prior to the processing.
-        Use the `vectorbt.records.base.Records.sort` method."""
-
-    def __init__(self,
-                 wrapper: ArrayWrapper,
-                 records_arr: tp.RecordArray,
-                 idx_field: str = 'idx',
-                 **kwargs) -> None:
-        Records.__init__(
-            self,
-            wrapper,
-            records_arr,
-            idx_field=idx_field,
-            **kwargs
-        )
+    """Extends `Records` for working with log records."""
 
-        if not all(field in records_arr.dtype.names for field in log_dt.names):
-            raise TypeError("Records array must match debug_info_dt")
-
-    @property  # no need for cached
-    def records_readable(self) -> tp.Frame:
-        """Records in readable format."""
-        df = self.records.copy()
-        df.columns = pd.MultiIndex.from_tuples([
-            ('Context', 'Log Id'),
-            ('Context', 'Date'),
-            ('Context', 'Column'),
-            ('Context', 'Group'),
-            ('Context', 'Cash'),
-            ('Context', 'Position'),
-            ('Context', 'Debt'),
-            ('Context', 'Free Cash'),
-            ('Context', 'Val Price'),
-            ('Context', 'Value'),
-            ('Order', 'Size'),
-            ('Order', 'Price'),
-            ('Order', 'Size Type'),
-            ('Order', 'Direction'),
-            ('Order', 'Fees'),
-            ('Order', 'Fixed Fees'),
-            ('Order', 'Slippage'),
-            ('Order', 'Min Size'),
-            ('Order', 'Max Size'),
-            ('Order', 'Rejection Prob'),
-            ('Order', 'Lock Cash'),
-            ('Order', 'Allow Partial'),
-            ('Order', 'Raise Rejection'),
-            ('Order', 'Log'),
-            ('New Context', 'Cash'),
-            ('New Context', 'Position'),
-            ('New Context', 'Debt'),
-            ('New Context', 'Free Cash'),
-            ('New Context', 'Val Price'),
-            ('New Context', 'Value'),
-            ('Order Result', 'Size'),
-            ('Order Result', 'Price'),
-            ('Order Result', 'Fees'),
-            ('Order Result', 'Side'),
-            ('Order Result', 'Status'),
-            ('Order Result', 'Status Info'),
-            ('Order Result', 'Order Id')
-        ])
-
-        df[('Context', 'Date')] = df[('Context', 'Date')].map(lambda x: self.wrapper.index[x])
-        df[('Context', 'Column')] = df[('Context', 'Column')].map(lambda x: self.wrapper.columns[x])
-        df[('Order', 'Size Type')] = map_enum_values(df[('Order', 'Size Type')], SizeType)
-        df[('Order', 'Direction')] = map_enum_values(df[('Order', 'Direction')], Direction)
-        df[('Order Result', 'Side')] = map_enum_values(df[('Order Result', 'Side')], OrderSide)
-        df[('Order Result', 'Status')] = map_enum_values(df[('Order Result', 'Status')], OrderStatus)
-        df[('Order Result', 'Status Info')] = map_enum_values(df[('Order Result', 'Status Info')], StatusInfo)
-        return df
+    @property
+    def field_config(self) -> Config:
+        return self._field_config
 
     # ############# Stats ############# #
 
     @property
     def stats_defaults(self) -> tp.Kwargs:
-        """Defaults for `Orders.stats`.
+        """Defaults for `Logs.stats`.
 
         Merges `vectorbt.generic.stats_builder.StatsBuilderMixin.stats_defaults` and
         `logs.stats` in `vectorbt._settings.settings`."""
@@ -241,4 +321,5 @@ def metrics(self) -> Config:
         return self._metrics
 
 
+Logs.override_field_config_doc(__pdoc__)
 Logs.override_metrics_doc(__pdoc__)
diff --git a/vectorbt/portfolio/nb.py b/vectorbt/portfolio/nb.py
index 9a7eea5a..33b387fc 100644
--- a/vectorbt/portfolio/nb.py
+++ b/vectorbt/portfolio/nb.py
@@ -117,12 +117,12 @@ def buy_nb(exec_state: ExecuteOrderState,
 
     if direction == Direction.LongOnly or direction == Direction.All:
         if cash_limit == 0:
-            return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.NoCashLong)
+            return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.NoCashLong)
         if np.isinf(size) and np.isinf(cash_limit):
             raise ValueError("Attempt to go in long direction infinitely")
     else:
         if exec_state.position == 0:
-            return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.NoOpenPosition)
+            return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.NoOpenPosition)
 
     # Get optimal order size
     if direction == Direction.ShortOnly:
@@ -131,11 +131,11 @@ def buy_nb(exec_state: ExecuteOrderState,
         adj_size = size
 
     if adj_size == 0:
-        return exec_state, order_not_filled_nb(OrderStatus.Ignored, StatusInfo.SizeZero)
+        return exec_state, order_not_filled_nb(OrderStatus.Ignored, OrderStatusInfo.SizeZero)
 
     if adj_size > max_size:
         if not allow_partial:
-            return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.MaxSizeExceeded)
+            return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.MaxSizeExceeded)
 
         adj_size = max_size
 
@@ -156,7 +156,7 @@ def buy_nb(exec_state: ExecuteOrderState,
         # to spend 100$ (cash_limit) in total
         new_req_cash = add_nb(cash_limit, -fixed_fees) / (1 + fees)
         if new_req_cash <= 0:
-            return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.CantCoverFees)
+            return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.CantCoverFees)
 
         final_size = new_req_cash / adj_price
         fees_paid = cash_limit - new_req_cash
@@ -164,11 +164,11 @@ def buy_nb(exec_state: ExecuteOrderState,
 
     # Check against minimum size
     if is_less_nb(final_size, min_size):
-        return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.MinSizeNotReached)
+        return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.MinSizeNotReached)
 
     # Check against partial fill (np.inf doesn't count)
     if np.isfinite(size) and is_less_nb(final_size, size) and not allow_partial:
-        return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.PartialFill)
+        return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.PartialFill)
 
     # Update current cash balance and position
     new_cash = add_nb(exec_state.cash, -final_cash)
@@ -237,7 +237,7 @@ def sell_nb(exec_state: ExecuteOrderState,
             if total_free_cash <= 0:
                 if exec_state.position <= 0:
                     # There is nothing to sell, and no free cash to short sell
-                    return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.NoCashShort)
+                    return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.NoCashShort)
 
                 # There is position to close, but no free cash to short sell
                 max_size_limit = long_size
@@ -246,7 +246,7 @@ def sell_nb(exec_state: ExecuteOrderState,
                 max_short_size = add_nb(total_free_cash, -fixed_fees) / (adj_price * (1 + fees))
                 max_size_limit = add_nb(long_size, max_short_size)
                 if max_size_limit <= 0:
-                    return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.CantCoverFees)
+                    return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.CantCoverFees)
 
             if lock_cash:
                 # Size has upper limit
@@ -270,7 +270,7 @@ def sell_nb(exec_state: ExecuteOrderState,
 
     if size_limit > max_size:
         if not allow_partial:
-            return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.MaxSizeExceeded)
+            return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.MaxSizeExceeded)
 
         size_limit = max_size
 
@@ -279,18 +279,18 @@ def sell_nb(exec_state: ExecuteOrderState,
             raise ValueError("Attempt to go in short direction infinitely")
     else:
         if exec_state.position == 0:
-            return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.NoOpenPosition)
+            return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.NoOpenPosition)
 
     if is_close_nb(size_limit, 0):
-        return exec_state, order_not_filled_nb(OrderStatus.Ignored, StatusInfo.SizeZero)
+        return exec_state, order_not_filled_nb(OrderStatus.Ignored, OrderStatusInfo.SizeZero)
 
     # Check against minimum size
     if is_less_nb(size_limit, min_size):
-        return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.MinSizeNotReached)
+        return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.MinSizeNotReached)
 
     # Check against partial fill
     if np.isfinite(size) and is_less_nb(size_limit, size) and not allow_partial:  # np.inf doesn't count
-        return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.PartialFill)
+        return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.PartialFill)
 
     # Get acquired cash
     acq_cash = size_limit * adj_price
@@ -301,7 +301,7 @@ def sell_nb(exec_state: ExecuteOrderState,
     # Get final cash by subtracting costs
     final_cash = add_nb(acq_cash, -fees_paid)
     if final_cash < 0:
-        return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.CantCoverFees)
+        return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.CantCoverFees)
 
     # Update current cash balance and position
     new_cash = exec_state.cash + final_cash
@@ -381,9 +381,9 @@ def execute_order_nb(state: ProcessOrderState, order: Order) -> tp.Tuple[Execute
 
     # Ignore order
     if np.isnan(order.size):
-        return exec_state, order_not_filled_nb(OrderStatus.Ignored, StatusInfo.SizeNaN)
+        return exec_state, order_not_filled_nb(OrderStatus.Ignored, OrderStatusInfo.SizeNaN)
     if np.isnan(order.price):
-        return exec_state, order_not_filled_nb(OrderStatus.Ignored, StatusInfo.PriceNaN)
+        return exec_state, order_not_filled_nb(OrderStatus.Ignored, OrderStatusInfo.PriceNaN)
 
     # Check execution state
     if np.isnan(cash) or cash < 0:
@@ -429,9 +429,9 @@ def execute_order_nb(state: ProcessOrderState, order: Order) -> tp.Tuple[Execute
     if order_size_type == SizeType.TargetPercent:
         # Target percentage of current value
         if np.isnan(value):
-            return exec_state, order_not_filled_nb(OrderStatus.Ignored, StatusInfo.ValueNaN)
+            return exec_state, order_not_filled_nb(OrderStatus.Ignored, OrderStatusInfo.ValueNaN)
         if value <= 0:
-            return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.ValueZeroNeg)
+            return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.ValueZeroNeg)
 
         order_size *= value
         order_size_type = SizeType.TargetValue
@@ -441,7 +441,7 @@ def execute_order_nb(state: ProcessOrderState, order: Order) -> tp.Tuple[Execute
         if np.isinf(val_price) or val_price <= 0:
             raise ValueError("val_price_now must be finite and greater than 0")
         if np.isnan(val_price):
-            return exec_state, order_not_filled_nb(OrderStatus.Ignored, StatusInfo.ValPriceNaN)
+            return exec_state, order_not_filled_nb(OrderStatus.Ignored, OrderStatusInfo.ValPriceNaN)
 
         order_size /= val_price
         if order_size_type == SizeType.Value:
@@ -501,7 +501,7 @@ def execute_order_nb(state: ProcessOrderState, order: Order) -> tp.Tuple[Execute
 
     if order.reject_prob > 0:
         if np.random.uniform(0, 1) < order.reject_prob:
-            return exec_state, order_not_filled_nb(OrderStatus.Rejected, StatusInfo.RandomEvent)
+            return exec_state, order_not_filled_nb(OrderStatus.Rejected, OrderStatusInfo.RandomEvent)
 
     return new_exec_state, order_result
 
@@ -539,20 +539,20 @@ def fill_log_record_nb(record: tp.Record,
     record['free_cash'] = free_cash
     record['val_price'] = val_price
     record['value'] = value
-    record['size'] = order.size
-    record['price'] = order.price
-    record['size_type'] = order.size_type
-    record['direction'] = order.direction
-    record['fees'] = order.fees
-    record['fixed_fees'] = order.fixed_fees
-    record['slippage'] = order.slippage
-    record['min_size'] = order.min_size
-    record['max_size'] = order.max_size
-    record['reject_prob'] = order.reject_prob
-    record['lock_cash'] = order.lock_cash
-    record['allow_partial'] = order.allow_partial
-    record['raise_reject'] = order.raise_reject
-    record['log'] = order.log
+    record['req_size'] = order.size
+    record['req_price'] = order.price
+    record['req_size_type'] = order.size_type
+    record['req_direction'] = order.direction
+    record['req_fees'] = order.fees
+    record['req_fixed_fees'] = order.fixed_fees
+    record['req_slippage'] = order.slippage
+    record['req_min_size'] = order.min_size
+    record['req_max_size'] = order.max_size
+    record['req_reject_prob'] = order.reject_prob
+    record['req_lock_cash'] = order.lock_cash
+    record['req_allow_partial'] = order.allow_partial
+    record['req_raise_reject'] = order.raise_reject
+    record['req_log'] = order.log
     record['new_cash'] = new_cash
     record['new_position'] = new_position
     record['new_debt'] = new_debt
@@ -589,33 +589,33 @@ def fill_order_record_nb(record: tp.Record,
 def raise_rejected_order_nb(order_result: OrderResult) -> None:
     """Raise an `vectorbt.portfolio.enums.RejectedOrderError`."""
 
-    if order_result.status_info == StatusInfo.SizeNaN:
+    if order_result.status_info == OrderStatusInfo.SizeNaN:
         raise RejectedOrderError("Size is NaN")
-    if order_result.status_info == StatusInfo.PriceNaN:
+    if order_result.status_info == OrderStatusInfo.PriceNaN:
         raise RejectedOrderError("Price is NaN")
-    if order_result.status_info == StatusInfo.ValPriceNaN:
+    if order_result.status_info == OrderStatusInfo.ValPriceNaN:
         raise RejectedOrderError("Asset valuation price is NaN")
-    if order_result.status_info == StatusInfo.ValueNaN:
+    if order_result.status_info == OrderStatusInfo.ValueNaN:
         raise RejectedOrderError("Asset/group value is NaN")
-    if order_result.status_info == StatusInfo.ValueZeroNeg:
+    if order_result.status_info == OrderStatusInfo.ValueZeroNeg:
         raise RejectedOrderError("Asset/group value is zero or negative")
-    if order_result.status_info == StatusInfo.SizeZero:
+    if order_result.status_info == OrderStatusInfo.SizeZero:
         raise RejectedOrderError("Size is zero")
-    if order_result.status_info == StatusInfo.NoCashShort:
+    if order_result.status_info == OrderStatusInfo.NoCashShort:
         raise RejectedOrderError("Not enough cash to short")
-    if order_result.status_info == StatusInfo.NoCashLong:
+    if order_result.status_info == OrderStatusInfo.NoCashLong:
         raise RejectedOrderError("Not enough cash to long")
-    if order_result.status_info == StatusInfo.NoOpenPosition:
+    if order_result.status_info == OrderStatusInfo.NoOpenPosition:
         raise RejectedOrderError("No open position to reduce/close")
-    if order_result.status_info == StatusInfo.MaxSizeExceeded:
+    if order_result.status_info == OrderStatusInfo.MaxSizeExceeded:
         raise RejectedOrderError("Size is greater than maximum allowed")
-    if order_result.status_info == StatusInfo.RandomEvent:
+    if order_result.status_info == OrderStatusInfo.RandomEvent:
         raise RejectedOrderError("Random event happened")
-    if order_result.status_info == StatusInfo.CantCoverFees:
+    if order_result.status_info == OrderStatusInfo.CantCoverFees:
         raise RejectedOrderError("Not enough cash to cover fees")
-    if order_result.status_info == StatusInfo.MinSizeNotReached:
+    if order_result.status_info == OrderStatusInfo.MinSizeNotReached:
         raise RejectedOrderError("Final size is less than minimum allowed")
-    if order_result.status_info == StatusInfo.PartialFill:
+    if order_result.status_info == OrderStatusInfo.PartialFill:
         raise RejectedOrderError("Final size is less than requested")
     raise RejectedOrderError
 
@@ -1445,11 +1445,11 @@ def simulate_nb(target_shape: tp.Shape,
            [1, 0, 0]])
     ```
 
-    ![](/vectorbt/docs/img/simulate_nb.gif)
+    ![](/docs/img/simulate_nb.gif)
 
     And here is the context information available at each step:
 
-    ![](/vectorbt/docs/img/context_info.png)
+    ![](/docs/img/context_info.png)
 
     ## Example
 
@@ -1627,7 +1627,7 @@ def simulate_nb(target_shape: tp.Shape,
     >>> Scatter(data=asset_value).fig.show()
     ```
 
-    ![](/vectorbt/docs/img/simulate_nb.svg)
+    ![](/docs/img/simulate_nb.svg)
 
     Note that the last order in a group with cash sharing is always disadvantaged
     as it has a bit less funds than the previous orders due to costs, which are not
@@ -2157,7 +2157,7 @@ def simulate_row_wise_nb(target_shape: tp.Shape,
 
     Let's illustrate the same example as in `simulate_nb` but adapted for this function:
 
-    ![](/vectorbt/docs/img/simulate_row_wise_nb.gif)
+    ![](/docs/img/simulate_row_wise_nb.gif)
 
     ## Example
 
@@ -3571,12 +3571,6 @@ def simulate_from_signals_nb(target_shape: tp.Shape,
 # ############# Trades ############# #
 
 
-@njit(cache=True)
-def trade_duration_map_nb(record: tp.Record) -> int:
-    """`map_func_nb` that returns trade duration."""
-    return record['exit_idx'] - record['entry_idx']
-
-
 @njit(cache=True)
 def trade_winning_streak_nb(records: tp.RecordArray) -> tp.Array1d:
     """Return the current winning streak of each trade."""
@@ -3649,7 +3643,7 @@ def fill_trade_record_nb(record: tp.Record,
     size_fraction = exit_size / entry_size_sum
     entry_fees = size_fraction * entry_fees_sum
 
-    # Get P&L and return
+    # Get PnL and return
     pnl, ret = get_trade_stats_nb(
         exit_size,
         entry_price,
@@ -3676,7 +3670,7 @@ def fill_trade_record_nb(record: tp.Record,
 
 
 @njit(cache=True)
-def orders_to_trades_nb(close: tp.Array2d, order_records: tp.RecordArray, col_map: tp.ColMap) -> tp.RecordArray:
+def trades_from_orders_nb(order_records: tp.RecordArray, close: tp.Array2d, col_map: tp.ColMap) -> tp.RecordArray:
     """Find trades and store their information as records to an array.
 
     ## Example
@@ -3687,7 +3681,7 @@ def orders_to_trades_nb(close: tp.Array2d, order_records: tp.RecordArray, col_ma
     >>> import pandas as pd
     >>> from numba import njit
     >>> from vectorbt.records.nb import col_map_nb
-    >>> from vectorbt.portfolio.nb import simulate_nb, order_nb, orders_to_trades_nb
+    >>> from vectorbt.portfolio.nb import simulate_nb, order_nb, trades_from_orders_nb
 
     >>> @njit
     ... def order_func_nb(c, order_size, order_price):
@@ -3729,7 +3723,7 @@ def orders_to_trades_nb(close: tp.Array2d, order_records: tp.RecordArray, col_ma
     ... )
 
     >>> col_map = col_map_nb(order_records['col'], target_shape[1])
-    >>> trade_records = orders_to_trades_nb(close, order_records, col_map)
+    >>> trade_records = trades_from_orders_nb(order_records, close, col_map)
     >>> pd.DataFrame.from_records(trade_records)
        id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
     0   0    0   1.0          0     1.101818    0.011018         2        2.97
@@ -3756,9 +3750,6 @@ def orders_to_trades_nb(close: tp.Array2d, order_records: tp.RecordArray, col_ma
     col_start_idxs = np.cumsum(col_lens) - col_lens
     records = np.empty(len(order_records), dtype=trade_dt)
     tidx = 0
-    entry_size_sum = 0.
-    entry_gross_sum = 0.
-    entry_fees_sum = 0.
     position_id = -1
 
     for col in range(col_lens.shape[0]):
@@ -3768,6 +3759,9 @@ def orders_to_trades_nb(close: tp.Array2d, order_records: tp.RecordArray, col_ma
         entry_idx = -1
         direction = -1
         last_id = -1
+        entry_size_sum = 0.
+        entry_gross_sum = 0.
+        entry_fees_sum = 0.
 
         for i in range(col_len):
             oidx = col_idxs[col_start_idxs[col] + i]
@@ -3969,17 +3963,17 @@ def copy_trade_record_nb(position_record: tp.Record, trade_record: tp.Record) ->
 
 
 @njit(cache=True)
-def trades_to_positions_nb(trade_records: tp.RecordArray, col_map: tp.ColMap) -> tp.RecordArray:
+def positions_from_trades_nb(trade_records: tp.RecordArray, col_map: tp.ColMap) -> tp.RecordArray:
     """Find positions and store their information as records to an array.
 
     ## Example
 
-    Building upon the example in `orders_to_trades_nb`, convert trades to positions:
+    Building upon the example in `trades_from_orders_nb`, convert trades to positions:
     ```python-repl
-    >>> from vectorbt.portfolio.nb import trades_to_positions_nb
+    >>> from vectorbt.portfolio.nb import positions_from_trades_nb
 
     >>> col_map = col_map_nb(trade_records['col'], target_shape[1])
-    >>> position_records = trades_to_positions_nb(trade_records, col_map)
+    >>> position_records = positions_from_trades_nb(trade_records, col_map)
     >>> pd.DataFrame.from_records(position_records)
        id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
     0   0    0   1.1          0     1.101818     0.01212         3    3.060000
diff --git a/vectorbt/portfolio/orders.py b/vectorbt/portfolio/orders.py
index 9ccbc6cf..3d362ebe 100644
--- a/vectorbt/portfolio/orders.py
+++ b/vectorbt/portfolio/orders.py
@@ -1,13 +1,7 @@
 """Base class for working with order records.
 
-Class `Orders` wraps order records and the corresponding time series (such as open or close)
-to analyze orders. Orders are mainly populated when simulating a portfolio and can be
-accessed as `vectorbt.portfolio.base.Portfolio.orders`.
-
-## Stats
-
-!!! hint
-    See `vectorbt.generic.stats_builder.StatsBuilderMixin.stats` and `Orders.metrics`.
+Order records capture information on filled orders. Orders are mainly populated when simulating
+a portfolio and can be accessed as `vectorbt.portfolio.base.Portfolio.orders`.
 
 ```python-repl
 >>> import pandas as pd
@@ -25,8 +19,26 @@
 ...     'b': np.random.uniform(-1, 1, size=100),
 ... }, index=[datetime(2020, 1, 1) + timedelta(days=i) for i in range(100)])
 >>> pf = vbt.Portfolio.from_orders(price, size, fees=0.01, freq='d')
+>>> orders = pf.orders
+
+>>> orders.buy.count()
+a    58
+b    51
+Name: count, dtype: int64
+
+>>> orders.sell.count()
+a    42
+b    49
+Name: count, dtype: int64
+```
+
+## Stats
+
+!!! hint
+    See `vectorbt.generic.stats_builder.StatsBuilderMixin.stats` and `Orders.metrics`.
 
->>> pf.orders.stats(column='a')
+```python-repl
+>>> orders['a'].stats()
 Start                2020-01-01 00:00:00
 End                  2020-04-09 00:00:00
 Period                 100 days 00:00:00
@@ -52,7 +64,7 @@
 `Orders.stats` also supports (re-)grouping:
 
 ```python-repl
->>> pf.orders.stats(group_by=True)
+>>> orders.stats(group_by=True)
 Start                2020-01-01 00:00:00
 End                  2020-04-09 00:00:00
 Period                 100 days 00:00:00
@@ -81,151 +93,115 @@
 import plotly.graph_objects as go
 
 from vectorbt import _typing as tp
-from vectorbt.utils.decorators import cached_property, cached_method
 from vectorbt.utils.colors import adjust_lightness
-from vectorbt.utils.enum import map_enum_values
 from vectorbt.utils.figure import make_figure
 from vectorbt.utils.config import merge_dicts, Config
-from vectorbt.base.reshape_fns import to_1d_array, to_2d_array, broadcast_to
+from vectorbt.base.reshape_fns import to_2d_array
 from vectorbt.base.array_wrapper import ArrayWrapper
 from vectorbt.generic.stats_builder import StatsBuilderMixin
 from vectorbt.records.base import Records
-from vectorbt.records.decorators import add_mapped_fields
+from vectorbt.records.decorators import attach_fields, override_field_config
 from vectorbt.portfolio.enums import order_dt, OrderSide
 
 __pdoc__ = {}
 
-orders_mf_config = Config(
+orders_field_config = Config(
     dict(
-        side=dict(defaults=dict(mapping=OrderSide))
+        dtype=order_dt,
+        settings=dict(
+            id=dict(
+                title='Order Id'
+            ),
+            size=dict(
+                title='Size'
+            ),
+            price=dict(
+                title='Price'
+            ),
+            fees=dict(
+                title='Fees'
+            ),
+            side=dict(
+                title='Side',
+                mapping=OrderSide
+            )
+        )
     ),
-    as_attrs=False,
     readonly=True,
-    copy_kwargs=dict(copy_mode='deep')
+    as_attrs=False
 )
 """_"""
 
-__pdoc__['orders_mf_config'] = f"""Config of `vectorbt.portfolio.enums.order_dt` 
-mapped fields to be overridden in `Orders`.
+__pdoc__['orders_field_config'] = f"""Field config for `Orders`.
 
 ```json
-{orders_mf_config.to_doc()}
+{orders_field_config.to_doc()}
 ```
 """
 
-OrdersT = tp.TypeVar("OrdersT", bound="Orders")
+orders_attach_field_config = Config(
+    dict(
+        side=dict(
+            attach_filters=True
+        )
+    ),
+    readonly=True,
+    as_attrs=False
+)
+"""_"""
 
+__pdoc__['orders_attach_field_config'] = f"""Config of fields to be attached to `Orders`.
 
-@add_mapped_fields(order_dt, orders_mf_config)
-class Orders(Records):
-    """Extends `Records` for working with order records.
+```json
+{orders_attach_field_config.to_doc()}
+```
+"""
 
-    ## Example
+OrdersT = tp.TypeVar("OrdersT", bound="Orders")
 
-    Get the total number of buy and sell operations:
-    ```python-repl
-    >>> import vectorbt as vbt
-    >>> import pandas as pd
 
-    >>> price = pd.Series([1., 2., 3., 2., 1.])
-    >>> size = pd.Series([1., 1., 1., 1., -1.])
-    >>> orders = vbt.Portfolio.from_orders(price, size).orders
+@attach_fields(orders_attach_field_config)
+@override_field_config(orders_field_config)
+class Orders(Records):
+    """Extends `Records` for working with order records."""
 
-    >>> orders.buy.count()
-    4
-    >>> orders.sell.count()
-    1
-    ```
-    """
+    @property
+    def field_config(self) -> Config:
+        return self._field_config
 
     def __init__(self,
                  wrapper: ArrayWrapper,
                  records_arr: tp.RecordArray,
-                 close: tp.ArrayLike,
-                 idx_field: str = 'idx',
+                 close: tp.Optional[tp.ArrayLike] = None,
                  **kwargs) -> None:
         Records.__init__(
             self,
             wrapper,
             records_arr,
-            idx_field=idx_field,
             close=close,
             **kwargs
         )
-        self._close = broadcast_to(close, wrapper.dummy(group_by=False))
-
-        if not all(field in records_arr.dtype.names for field in order_dt.names):
-            raise TypeError("Records array must match order_dt")
+        self._close = close
 
-    def indexing_func_meta(self: OrdersT, pd_indexing_func: tp.PandasIndexingFunc,
-                           **kwargs) -> tp.Tuple[OrdersT, tp.MaybeArray, tp.Array1d]:
-        """Perform indexing on `Orders` and return metadata."""
+    def indexing_func(self: OrdersT, pd_indexing_func: tp.PandasIndexingFunc, **kwargs) -> OrdersT:
+        """Perform indexing on `Orders`."""
         new_wrapper, new_records_arr, group_idxs, col_idxs = \
             Records.indexing_func_meta(self, pd_indexing_func, **kwargs)
-        new_close = new_wrapper.wrap(to_2d_array(self.close)[:, col_idxs], group_by=False)
+        if self.close is not None:
+            new_close = new_wrapper.wrap(to_2d_array(self.close)[:, col_idxs], group_by=False)
+        else:
+            new_close = None
         return self.copy(
             wrapper=new_wrapper,
             records_arr=new_records_arr,
             close=new_close
-        ), group_idxs, col_idxs
-
-    def indexing_func(self: OrdersT, pd_indexing_func: tp.PandasIndexingFunc, **kwargs) -> OrdersT:
-        """Perform indexing on `Orders`."""
-        return self.indexing_func_meta(pd_indexing_func, **kwargs)[0]
+        )
 
     @property
     def close(self) -> tp.SeriesFrame:
-        """Reference price such as close."""
+        """Reference price such as close (optional)."""
         return self._close
 
-    @property  # no need for cached
-    def records_readable(self) -> tp.Frame:
-        """Records in readable format."""
-        df = self.records.copy()
-        df.columns = [
-            'Order Id',
-            'Date',
-            'Column',
-            'Size',
-            'Price',
-            'Fees',
-            'Side'
-        ]
-        df['Date'] = df['Date'].map(lambda x: self.wrapper.index[x])
-        df['Column'] = df['Column'].map(lambda x: self.wrapper.columns[x])
-        df['Side'] = map_enum_values(df['Side'], OrderSide)
-        return df
-
-    # ############# OrderSide ############# #
-
-    @cached_property
-    def buy(self: OrdersT) -> OrdersT:
-        """Buy operations."""
-        filter_mask = self.values['side'] == OrderSide.Buy
-        return self.filter_by_mask(filter_mask)
-
-    @cached_method
-    def buy_rate(self, group_by: tp.GroupByLike = None, wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Rate of buy operations."""
-        buy_count = to_1d_array(self.buy.count(group_by=group_by))
-        total_count = to_1d_array(self.count(group_by=group_by))
-        wrap_kwargs = merge_dicts(dict(name_or_index='buy_rate'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(buy_count / total_count, group_by=group_by, **wrap_kwargs)
-
-    @cached_property
-    def sell(self: OrdersT) -> OrdersT:
-        """Sell operations."""
-        filter_mask = self.values['side'] == OrderSide.Sell
-        return self.filter_by_mask(filter_mask)
-
-    @cached_method
-    def sell_rate(self, group_by: tp.GroupByLike = None, wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Rate of sell operations."""
-        sell_count = to_1d_array(self.sell.count(group_by=group_by))
-        total_count = to_1d_array(self.count(group_by=group_by))
-        wrap_kwargs = merge_dicts(dict(name_or_index='sell_rate'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(sell_count / total_count, group_by=group_by, **wrap_kwargs)
-
     # ############# Stats ############# #
 
     @property
@@ -355,7 +331,6 @@ def metrics(self) -> Config:
 
     def plot(self,
              column: tp.Optional[tp.Label] = None,
-             plot_close: bool = True,
              close_trace_kwargs: tp.KwargsLike = None,
              buy_trace_kwargs: tp.KwargsLike = None,
              sell_trace_kwargs: tp.KwargsLike = None,
@@ -366,7 +341,6 @@ def plot(self,
 
         Args:
             column (str): Name of the column to plot.
-            plot_close (bool): Whether to plot `Orders.close`.
             close_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for `Orders.close`.
             buy_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for "Buy" markers.
             sell_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for "Sell" markers.
@@ -377,10 +351,19 @@ def plot(self,
         ## Example
 
         ```python-repl
+        >>> import pandas as pd
+        >>> from datetime import datetime, timedelta
+        >>> import vectorbt as vbt
+
+        >>> price = pd.Series([1., 2., 3., 2., 1.], name='Price')
+        >>> price.index = [datetime(2020, 1, 1) + timedelta(days=i) for i in range(len(price))]
+        >>> size = pd.Series([1., 1., 1., 1., -1.])
+        >>> orders = vbt.Portfolio.from_orders(price, size).orders
+
         >>> orders.plot()
         ```
 
-        ![](/vectorbt/docs/img/orders_plot.svg)"""
+        ![](/docs/img/orders_plot.svg)"""
         from vectorbt._settings import settings
         plotting_cfg = settings['plotting']
 
@@ -405,74 +388,95 @@ def plot(self,
             fig = make_figure()
         fig.update_layout(**layout_kwargs)
 
-        # Plot close
-        if plot_close:
+        # Plot price
+        if self_col.close is not None:
             fig = self_col.close.vbt.plot(trace_kwargs=close_trace_kwargs, add_trace_kwargs=add_trace_kwargs, fig=fig)
 
-        if len(self_col.values) > 0:
+        if self_col.count() > 0:
             # Extract information
-            _id = self_col.values['id']
-            idx = self_col.values['idx']
-            size = self_col.values['size']
-            price = self_col.values['price']
-            fees = self_col.values['fees']
-            side = self_col.values['side']
-
-            # Plot Buy markers
+            id_ = self_col.resolve_field_arr('id')
+            id_title = self_col.resolve_field_title('id')
+
+            idx = self_col.resolve_map_field_to_index('idx')
+            idx_title = self_col.resolve_field_title('idx')
+
+            size = self_col.resolve_field_arr('size')
+            size_title = self_col.resolve_field_title('size')
+
+            fees = self_col.resolve_field_arr('fees')
+            fees_title = self_col.resolve_field_title('fees')
+
+            price = self_col.resolve_field_arr('price')
+            price_title = self_col.resolve_field_title('price')
+
+            side = self_col.resolve_field_arr('side')
+
             buy_mask = side == OrderSide.Buy
-            buy_customdata = np.stack((_id[buy_mask], size[buy_mask], fees[buy_mask]), axis=1)
-            buy_scatter = go.Scatter(
-                x=self_col.wrapper.index[idx[buy_mask]],
-                y=price[buy_mask],
-                mode='markers',
-                marker=dict(
-                    symbol='triangle-up',
-                    color=plotting_cfg['contrast_color_schema']['green'],
-                    size=8,
-                    line=dict(
-                        width=1,
-                        color=adjust_lightness(plotting_cfg['contrast_color_schema']['green'])
-                    )
-                ),
-                name='Buy',
-                customdata=buy_customdata,
-                hovertemplate="Order Id: %{customdata[0]}"
-                              "<br>Date: %{x}"
-                              "<br>Price: %{y}"
-                              "<br>Size: %{customdata[1]:.6f}"
-                              "<br>Fees: %{customdata[2]:.6f}"
-            )
-            buy_scatter.update(**buy_trace_kwargs)
-            fig.add_trace(buy_scatter, **add_trace_kwargs)
+            if buy_mask.any():
+                # Plot buy markers
+                buy_customdata = np.stack((
+                    id_[buy_mask],
+                    size[buy_mask],
+                    fees[buy_mask]
+                ), axis=1)
+                buy_scatter = go.Scatter(
+                    x=idx[buy_mask],
+                    y=price[buy_mask],
+                    mode='markers',
+                    marker=dict(
+                        symbol='triangle-up',
+                        color=plotting_cfg['contrast_color_schema']['green'],
+                        size=8,
+                        line=dict(
+                            width=1,
+                            color=adjust_lightness(plotting_cfg['contrast_color_schema']['green'])
+                        )
+                    ),
+                    name='Buy',
+                    customdata=buy_customdata,
+                    hovertemplate=f"{id_title}: %{{customdata[0]}}"
+                                  f"<br>{idx_title}: %{{x}}"
+                                  f"<br>{price_title}: %{{y}}"
+                                  f"<br>{size_title}: %{{customdata[1]:.6f}}"
+                                  f"<br>{fees_title}: %{{customdata[2]:.6f}}"
+                )
+                buy_scatter.update(**buy_trace_kwargs)
+                fig.add_trace(buy_scatter, **add_trace_kwargs)
 
-            # Plot Sell markers
             sell_mask = side == OrderSide.Sell
-            sell_customdata = np.stack((_id[sell_mask], size[sell_mask], fees[sell_mask]), axis=1)
-            sell_scatter = go.Scatter(
-                x=self_col.wrapper.index[idx[sell_mask]],
-                y=price[sell_mask],
-                mode='markers',
-                marker=dict(
-                    symbol='triangle-down',
-                    color=plotting_cfg['contrast_color_schema']['red'],
-                    size=8,
-                    line=dict(
-                        width=1,
-                        color=adjust_lightness(plotting_cfg['contrast_color_schema']['red'])
-                    )
-                ),
-                name='Sell',
-                customdata=sell_customdata,
-                hovertemplate="Order Id: %{customdata[0]}"
-                              "<br>Date: %{x}"
-                              "<br>Price: %{y}"
-                              "<br>Size: %{customdata[1]:.6f}"
-                              "<br>Fees: %{customdata[2]:.6f}"
-            )
-            sell_scatter.update(**sell_trace_kwargs)
-            fig.add_trace(sell_scatter, **add_trace_kwargs)
+            if sell_mask.any():
+                # Plot sell markers
+                sell_customdata = np.stack((
+                    id_[sell_mask],
+                    size[sell_mask],
+                    fees[sell_mask]
+                ), axis=1)
+                sell_scatter = go.Scatter(
+                    x=idx[sell_mask],
+                    y=price[sell_mask],
+                    mode='markers',
+                    marker=dict(
+                        symbol='triangle-down',
+                        color=plotting_cfg['contrast_color_schema']['red'],
+                        size=8,
+                        line=dict(
+                            width=1,
+                            color=adjust_lightness(plotting_cfg['contrast_color_schema']['red'])
+                        )
+                    ),
+                    name='Sell',
+                    customdata=sell_customdata,
+                    hovertemplate=f"{id_title}: %{{customdata[0]}}"
+                                  f"<br>{idx_title}: %{{x}}"
+                                  f"<br>{price_title}: %{{y}}"
+                                  f"<br>{size_title}: %{{customdata[1]:.6f}}"
+                                  f"<br>{fees_title}: %{{customdata[2]:.6f}}"
+                )
+                sell_scatter.update(**sell_trace_kwargs)
+                fig.add_trace(sell_scatter, **add_trace_kwargs)
 
         return fig
 
 
+Orders.override_field_config_doc(__pdoc__)
 Orders.override_metrics_doc(__pdoc__)
diff --git a/vectorbt/portfolio/trades.py b/vectorbt/portfolio/trades.py
index 487e61ce..1d0b2258 100644
--- a/vectorbt/portfolio/trades.py
+++ b/vectorbt/portfolio/trades.py
@@ -1,21 +1,32 @@
 """Base class for working with trade records.
 
-Class `Trades` wraps trade records and the corresponding time series
-(such as open or close) to analyze trades. Use `vectorbt.portfolio.trades.Trades.from_orders`
+Trade records capture information on trades. Use `vectorbt.portfolio.trades.Trades.from_orders`
 to generate trade records from order records. This is done automatically in the
 `vectorbt.portfolio.base.Portfolio` class, available as `vectorbt.portfolio.base.Portfolio.trades`.
 
-Class `Positions` has the same properties as trades and is also
+Position records capture information on positions and are also
 provided by `vectorbt.portfolio.base.Portfolio` as `vectorbt.portfolio.base.Portfolio.positions`.
 
 !!! warning
     Both record types return both closed AND open trades, which may skew your performance results.
     To only consider closed trades, you should explicitly query `closed` attribute.
 
-## Stats
+## Classes
 
-!!! hint
-    See `vectorbt.generic.stats_builder.StatsBuilderMixin.stats` and `Trades.metrics`.
+### Trades
+
+In vectorbt, a trade is a partial closing operation; it's is a more fine-grained representation
+of a position. One position can incorporate multiple trades. Performance for this operation is
+calculated based on the size-weighted average of previous opening operations within the same
+position. The PnL of all trades combined always equals to the PnL of the entire position.
+
+For example, if you have a single large buy operation and 100 small sell operations, you will see
+100 trades, each opening with a fraction of the buy operation's size and fees. On the other hand,
+having 100 buy operations and just a single sell operation will generate a single trade with buy
+price being a size-weighted average over all purchase prices, and opening size and fees being
+the sum over all sizes and fees.
+
+Increasing position:
 
 ```python-repl
 >>> import pandas as pd
@@ -23,6 +34,144 @@
 >>> from datetime import datetime, timedelta
 >>> import vectorbt as vbt
 
+>>> vbt.Portfolio.from_orders(
+...     pd.Series([1., 2., 3., 4., 5.]),
+...     pd.Series([1., 1., 1., 1., -4.]),
+...     fixed_fees=1.).trades.records
+   id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
+0   0    0   4.0          0          2.5         4.0         4         5.0
+
+   exit_fees  pnl  return  direction  status  position_id
+0        1.0  5.0     0.5          0       1            0
+```
+
+Decreasing position:
+
+```python-repl
+>>> vbt.Portfolio.from_orders(
+...     pd.Series([1., 2., 3., 4., 5.]),
+...     pd.Series([4., -1., -1., -1., -1.]),
+...     fixed_fees=1.).trades.records
+   id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
+0   0    0   1.0          0          1.0        0.25         1         2.0
+1   1    0   1.0          0          1.0        0.25         2         3.0
+2   2    0   1.0          0          1.0        0.25         3         4.0
+3   3    0   1.0          0          1.0        0.25         4         5.0
+
+   exit_fees   pnl  return  direction  status  position_id
+0        1.0 -0.25   -0.25          0       1            0
+1        1.0  0.75    0.75          0       1            0
+2        1.0  1.75    1.75          0       1            0
+3        1.0  2.75    2.75          0       1            0
+```
+
+Multiple reversing positions:
+
+```python-repl
+>>> vbt.Portfolio.from_orders(
+...     pd.Series([1., 2., 3., 4., 5.]),
+...     pd.Series([1., -2., 2., -2., 1.]),
+...     fixed_fees=1.).trades.records
+   id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
+0   0    0   1.0          0          1.0         1.0         1         2.0
+1   1    0   1.0          1          2.0         0.5         2         3.0
+2   2    0   1.0          2          3.0         0.5         3         4.0
+3   3    0   1.0          3          4.0         0.5         4         5.0
+
+   exit_fees  pnl  return  direction  status  position_id
+0        0.5 -0.5  -0.500          0       1            0
+1        0.5 -2.0  -1.000          1       1            1
+2        0.5  0.0   0.000          0       1            2
+3        1.0 -2.5  -0.625          1       1            3
+```
+
+Get count and PnL of trades:
+
+```python-repl
+>>> price = pd.Series([1., 2., 3., 4., 3., 2., 1.])
+>>> orders = pd.Series([1., -0.5, -0.5, 2., -0.5, -0.5, -0.5])
+>>> pf = vbt.Portfolio.from_orders(price, orders)
+
+>>> trades = vbt.Trades.from_orders(pf.orders, price)
+>>> trades.count()
+6
+>>> trades.pnl.sum()
+-3.0
+>>> trades.winning.count()
+2
+>>> trades.winning.pnl.sum()
+1.5
+```
+
+Get count and PnL of trades with duration of more than 2 days:
+```python-repl
+>>> mask = (trades.records['exit_idx'] - trades.records['entry_idx']) > 2
+>>> trades_filtered = trades.apply_mask(mask)
+>>> trades_filtered.count()
+2
+>>> trades_filtered.pnl.sum()
+-3.0
+```
+
+### Positions
+
+In vectorbt, a position aggregates one or multiple trades sharing the same column
+and position index. It has the same layout as a trade.
+
+Increasing position:
+
+```python-repl
+>>> vbt.Portfolio.from_orders(
+...     pd.Series([1., 2., 3., 4., 5.]),
+...     pd.Series([1., 1., 1., 1., -4.]),
+...     fixed_fees=1.).positions.records
+   id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
+0   0    0   4.0          0          2.5         4.0         4         5.0
+
+   exit_fees  pnl  return  direction  status
+0        1.0  5.0     0.5          0       1
+```
+
+Decreasing position:
+
+```python-repl
+>>> vbt.Portfolio.from_orders(
+...     pd.Series([1., 2., 3., 4., 5.]),
+...     pd.Series([4., -1., -1., -1., -1.]),
+...     fixed_fees=1.).positions.records
+   id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
+0   0    0   4.0          0          1.0         1.0         4         3.5
+
+   exit_fees  pnl  return  direction  status
+0        4.0  5.0    1.25          0       1
+```
+
+Multiple positions:
+
+```python-repl
+>>> vbt.Portfolio.from_orders(
+...     pd.Series([1., 2., 3., 4., 5.]),
+...     pd.Series([1., -2., 2., -2., 1.]),
+...     fixed_fees=1.).positions.records
+   id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
+0   0    0   1.0          0          1.0         1.0         1         2.0
+1   1    0   1.0          1          2.0         0.5         2         3.0
+2   2    0   1.0          2          3.0         0.5         3         4.0
+3   3    0   1.0          3          4.0         0.5         4         5.0
+
+   exit_fees  pnl  return  direction  status
+0        0.5 -0.5  -0.500          0       1
+1        0.5 -2.0  -1.000          1       1
+2        0.5  0.0   0.000          0       1
+3        1.0 -2.5  -0.625          1       1
+```
+
+## Stats
+
+!!! hint
+    See `vectorbt.generic.stats_builder.StatsBuilderMixin.stats` and `Trades.metrics`.
+
+```python-repl
 >>> np.random.seed(42)
 >>> price = pd.DataFrame({
 ...     'a': np.random.uniform(1, 2, size=100),
@@ -34,18 +183,20 @@
 ... }, index=[datetime(2020, 1, 1) + timedelta(days=i) for i in range(100)])
 >>> pf = vbt.Portfolio.from_orders(price, size, fees=0.01, freq='d')
 
->>> pf.trades.stats(column='a')
+>>> pf.trades['a'].stats()
 Start                                2020-01-01 00:00:00
 End                                  2020-04-09 00:00:00
 Period                                 100 days 00:00:00
 First Trade Start                    2020-01-01 00:00:00
 Last Trade End                       2020-04-09 00:00:00
+Coverage                               100 days 00:00:00
+Overlap Coverage                        97 days 00:00:00
 Total Records                                         48
 Total Long Trades                                     22
 Total Short Trades                                    26
 Total Closed Trades                                   47
 Total Open Trades                                      1
-Open Trade P&L                                 -1.290981
+Open Trade PnL                                 -1.290981
 Win Rate [%]                                    51.06383
 Max Win Streak                                         3
 Max Loss Streak                                        3
@@ -64,10 +215,11 @@
 Positions share almost identical metrics with trades:
 
 ```python-repl
->>> pf.positions.stats(column='a')
+>>> pf.positions['a'].stats()
 Start                            2020-01-01 00:00:00
 End                              2020-04-09 00:00:00
 Period                             100 days 00:00:00
+Coverage [%]                                   100.0
 First Position Start             2020-01-01 00:00:00
 Last Position End                2020-04-09 00:00:00
 Total Records                                      3
@@ -75,7 +227,7 @@
 Total Short Positions                              1
 Total Closed Positions                             2
 Total Open Positions                               1
-Open Position P&L                          -0.929746
+Open Position PnL                          -0.929746
 Win Rate [%]                                    50.0
 Max Win Streak                                     1
 Max Loss Streak                                    1
@@ -88,26 +240,27 @@
 Profit Factor                               0.261748
 Expectancy                                 -0.217492
 SQN                                        -0.585103
-Coverage [%]                                    99.0
 Name: a, dtype: object
 ```
 
 To also include open trades/positions when calculating metrics such as win rate, pass `incl_open=True`:
 
 ```python-repl
->>> pf.trades.stats(column='a', settings=dict(incl_open=True))
+>>> pf.trades['a'].stats(settings=dict(incl_open=True))
 Start                         2020-01-01 00:00:00
 End                           2020-04-09 00:00:00
 Period                          100 days 00:00:00
 First Trade Start             2020-01-01 00:00:00
 Last Trade End                2020-04-09 00:00:00
+Coverage                        100 days 00:00:00
+Overlap Coverage                 97 days 00:00:00
 Total Records                                  48
 Total Long Trades                              22
 Total Short Trades                             26
 Total Closed Trades                            47
 Total Open Trades                               1
-Open Trade P&L                          -1.290981
-Win Rate [%]                                 50.0
+Open Trade PnL                          -1.290981
+Win Rate [%]                             51.06383
 Max Win Streak                                  3
 Max Loss Streak                                 3
 Best Trade [%]                          43.326077
@@ -115,7 +268,7 @@
 Avg Winning Trade [%]                   21.418522
 Avg Losing Trade [%]                   -19.117677
 Avg Winning Trade Duration       22 days 22:00:00
-Avg Losing Trade Duration        29 days 23:00:00
+Avg Losing Trade Duration        30 days 00:00:00
 Profit Factor                            0.693135
 Expectancy                              -0.028432
 SQN                                     -0.794284
@@ -131,12 +284,14 @@
 Period                                 100 days 00:00:00
 First Trade Start                    2020-01-01 00:00:00
 Last Trade End                       2020-04-09 00:00:00
+Coverage                               100 days 00:00:00
+Overlap Coverage                       100 days 00:00:00
 Total Records                                        104
 Total Long Trades                                     32
 Total Short Trades                                    72
 Total Closed Trades                                  102
 Total Open Trades                                      2
-Open Trade P&L                                 -1.790938
+Open Trade PnL                                 -1.790938
 Win Rate [%]                                   46.078431
 Max Win Streak                                         5
 Max Loss Streak                                        5
@@ -159,279 +314,216 @@
 
 from vectorbt import _typing as tp
 from vectorbt.utils.colors import adjust_lightness
-from vectorbt.utils.decorators import cached_property, cached_method
 from vectorbt.utils.config import merge_dicts, Config
-from vectorbt.utils.datetime import DatetimeIndexes
-from vectorbt.utils.enum import map_enum_values
 from vectorbt.utils.figure import make_figure, get_domain
 from vectorbt.utils.array import min_rel_rescale, max_rel_rescale
-from vectorbt.utils.template import RepEval, Rep
-from vectorbt.base.reshape_fns import to_1d_array, to_2d_array, broadcast_to
+from vectorbt.utils.template import RepEval
+from vectorbt.utils.decorators import cached_method, cached_property
+from vectorbt.base.reshape_fns import to_1d_array, to_2d_array
 from vectorbt.base.array_wrapper import ArrayWrapper
 from vectorbt.generic.stats_builder import StatsBuilderMixin
-from vectorbt.records.base import Records
+from vectorbt.generic.ranges import Ranges
+from vectorbt.records.decorators import attach_fields, override_field_config
 from vectorbt.records.mapped_array import MappedArray
-from vectorbt.records.decorators import add_mapped_fields
-from vectorbt.portfolio.enums import TradeDirection, TradeStatus, trade_dt, position_dt, TradeType
+from vectorbt.portfolio.enums import TradeDirection, TradeStatus, trade_dt, position_dt
 from vectorbt.portfolio import nb
 from vectorbt.portfolio.orders import Orders
 
+__pdoc__ = {}
+
 # ############# Trades ############# #
 
-__pdoc__ = {}
+trades_field_config = Config(
+    dict(
+        dtype=trade_dt,
+        settings={
+            'id': dict(
+                title='Trade Id'
+            ),
+            'idx': dict(
+                name='exit_idx'  # remap field of Records
+            ),
+            'start_idx': dict(
+                name='entry_idx'  # remap field of Ranges
+            ),
+            'end_idx': dict(
+                name='exit_idx'  # remap field of Ranges
+            ),
+            'size': dict(
+                title='Size'
+            ),
+            'entry_idx': dict(
+                title='Entry Date',
+                mapping='index'
+            ),
+            'entry_price': dict(
+                title='Avg Entry Price'
+            ),
+            'entry_fees': dict(
+                title='Entry Fees'
+            ),
+            'exit_idx': dict(
+                title='Exit Date',
+                mapping='index'
+            ),
+            'exit_price': dict(
+                title='Avg Exit Price'
+            ),
+            'exit_fees': dict(
+                title='Exit Fees'
+            ),
+            'pnl': dict(
+                title='PnL'
+            ),
+            'return': dict(
+                title='Return'
+            ),
+            'direction': dict(
+                title='Direction',
+                mapping=TradeDirection
+            ),
+            'status': dict(
+                title='Status',
+                mapping=TradeStatus
+            ),
+            'position_id': dict(
+                title='Position Id',
+                plot=True
+            )
+        }
+    ),
+    readonly=True,
+    as_attrs=False
+)
+"""_"""
+
+__pdoc__['trades_field_config'] = f"""Field config for `Trades`.
 
-trades_mf_config = Config(
+```json
+{trades_field_config.to_doc()}
+```
+"""
+
+trades_attach_field_config = Config(
     {
-        'return': dict(target_name='returns'),
-        'direction': dict(defaults=dict(mapping=TradeDirection)),
-        'status': dict(defaults=dict(mapping=TradeStatus))
+        'return': dict(
+            attach='returns'
+        ),
+        'direction': dict(
+            attach_filters=True
+        ),
+        'status': dict(
+            attach_filters=True,
+            on_conflict='ignore'
+        )
     },
-    as_attrs=False,
     readonly=True,
-    copy_kwargs=dict(copy_mode='deep')
+    as_attrs=False
 )
 """_"""
 
-__pdoc__['trades_mf_config'] = f"""Config of `vectorbt.portfolio.enums.trade_dt` 
-mapped fields to be overridden in `Trades`.
+__pdoc__['trades_attach_field_config'] = f"""Config of fields to be attached to `Trades`.
 
 ```json
-{trades_mf_config.to_doc()}
+{trades_attach_field_config.to_doc()}
 ```
 """
 
 TradesT = tp.TypeVar("TradesT", bound="Trades")
 
 
-@add_mapped_fields(trade_dt, trades_mf_config)
-class Trades(Records):
-    """Extends `Records` for working with trade records.
-
-    In vectorbt, a trade is a partial closing operation; it's is a more fine-grained representation
-    of a position. One position can incorporate multiple trades. Performance for this operation is
-    calculated based on the size-weighted average of previous opening operations within the same
-    position. The PnL of all trades combined always equals to the PnL of the entire position.
-
-    For example, if you have a single large buy operation and 100 small sell operations, you will see
-    100 trades, each opening with a fraction of the buy operation's size and fees. On the other hand,
-    having 100 buy operations and just a single sell operation will generate a single trade with buy
-    price being a size-weighted average over all purchase prices, and opening size and fees being
-    the sum over all sizes and fees.
-
-    ## Example
-
-    Increasing position:
-    ```python-repl
-    >>> import vectorbt as vbt
-    >>> import pandas as pd
-
-    >>> vbt.Portfolio.from_orders(
-    ...     pd.Series([1., 2., 3., 4., 5.]),
-    ...     pd.Series([1., 1., 1., 1., -4.]),
-    ...     fixed_fees=1.).trades.records
-       id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
-    0   0    0   4.0          0          2.5         4.0         4         5.0
-
-       exit_fees  pnl  return  direction  status  position_id
-    0        1.0  5.0     0.5          0       1            0
-    ```
-
-    Decreasing position:
-    ```python-repl
-    >>> vbt.Portfolio.from_orders(
-    ...     pd.Series([1., 2., 3., 4., 5.]),
-    ...     pd.Series([4., -1., -1., -1., -1.]),
-    ...     fixed_fees=1.).trades.records
-       id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
-    0   0    0   1.0          0          1.0        0.25         1         2.0
-    1   1    0   1.0          0          1.0        0.25         2         3.0
-    2   2    0   1.0          0          1.0        0.25         3         4.0
-    3   3    0   1.0          0          1.0        0.25         4         5.0
-
-       exit_fees   pnl  return  direction  status  position_id
-    0        1.0 -0.25   -0.25          0       1            0
-    1        1.0  0.75    0.75          0       1            0
-    2        1.0  1.75    1.75          0       1            0
-    3        1.0  2.75    2.75          0       1            0
-    ```
-
-    Multiple reversing positions:
-    ```python-repl
-    >>> vbt.Portfolio.from_orders(
-    ...     pd.Series([1., 2., 3., 4., 5.]),
-    ...     pd.Series([1., -2., 2., -2., 1.]),
-    ...     fixed_fees=1.).trades.records
-       id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
-    0   0    0   1.0          0          1.0         1.0         1         2.0
-    1   1    0   1.0          1          2.0         0.5         2         3.0
-    2   2    0   1.0          2          3.0         0.5         3         4.0
-    3   3    0   1.0          3          4.0         0.5         4         5.0
-
-       exit_fees  pnl  return  direction  status  position_id
-    0        0.5 -0.5  -0.500          0       1            0
-    1        0.5 -2.0  -1.000          1       1            1
-    2        0.5  0.0   0.000          0       1            2
-    3        1.0 -2.5  -0.625          1       1            3
-    ```
-
-    Get count and PnL of trades:
-    ```python-repl
-    >>> price = pd.Series([1., 2., 3., 4., 3., 2., 1.])
-    >>> orders = pd.Series([1., -0.5, -0.5, 2., -0.5, -0.5, -0.5])
-    >>> pf = vbt.Portfolio.from_orders(price, orders)
-
-    >>> trades = vbt.Trades.from_orders(pf.orders)
-    >>> trades.count()
-    6
-    >>> trades.pnl.sum()
-    -3.0
-    >>> trades.winning.count()
-    2
-    >>> trades.winning.pnl.sum()
-    1.5
-    ```
-
-    Get count and PnL of trades with duration of more than 2 days:
-    ```python-repl
-    >>> mask = (trades.records['exit_idx'] - trades.records['entry_idx']) > 2
-    >>> trades_filtered = trades.filter_by_mask(mask)
-    >>> trades_filtered.count()
-    2
-    >>> trades_filtered.pnl.sum()
-    -3.0
-    ```
-    """
-    trade_type: tp.ClassVar[int] = TradeType.Trade
+@attach_fields(trades_attach_field_config)
+@override_field_config(trades_field_config)
+class Trades(Ranges):
+    """Extends `vectorbt.generic.ranges.Ranges` for working with trade records."""
+
+    @property
+    def field_config(self) -> Config:
+        return self._field_config
 
     def __init__(self,
                  wrapper: ArrayWrapper,
                  records_arr: tp.RecordArray,
                  close: tp.ArrayLike,
-                 idx_field: str = 'exit_idx',
                  **kwargs) -> None:
-        Records.__init__(
+        Ranges.__init__(
             self,
             wrapper,
             records_arr,
-            idx_field=idx_field,
             close=close,
             **kwargs
         )
-        self._close = broadcast_to(close, wrapper.dummy(group_by=False))
-
-        if self.trade_type == TradeType.Trade:
-            if not all(field in records_arr.dtype.names for field in trade_dt.names):
-                raise TypeError("Records array must match trade_dt")
-        else:
-            if not all(field in records_arr.dtype.names for field in position_dt.names):
-                raise TypeError("Records array must match position_dt")
+        self._close = close
 
-    def indexing_func_meta(self: TradesT, pd_indexing_func: tp.PandasIndexingFunc,
-                           **kwargs) -> tp.Tuple[TradesT, tp.MaybeArray, tp.Array1d]:
-        """Perform indexing on `Trades` and also return metadata."""
+    def indexing_func(self: TradesT, pd_indexing_func: tp.PandasIndexingFunc, **kwargs) -> TradesT:
+        """Perform indexing on `Trades`."""
         new_wrapper, new_records_arr, group_idxs, col_idxs = \
-            Records.indexing_func_meta(self, pd_indexing_func, **kwargs)
-        new_close = new_wrapper.wrap(to_2d_array(self.close)[:, col_idxs], group_by=False)
+            Ranges.indexing_func_meta(self, pd_indexing_func, **kwargs)
+        if self.close is not None:
+            new_close = new_wrapper.wrap(to_2d_array(self.close)[:, col_idxs], group_by=False)
+        else:
+            new_close = None
         return self.copy(
             wrapper=new_wrapper,
             records_arr=new_records_arr,
             close=new_close
-        ), group_idxs, col_idxs
-
-    def indexing_func(self: TradesT, pd_indexing_func: tp.PandasIndexingFunc, **kwargs) -> TradesT:
-        """Perform indexing on `Trades`."""
-        return self.indexing_func_meta(pd_indexing_func, **kwargs)[0]
+        )
 
     @property
     def close(self) -> tp.SeriesFrame:
-        """Reference price such as close."""
+        """Reference price such as close (optional)."""
         return self._close
 
-    @property
-    def is_positions(self) -> bool:
-        """Whether this object stores positions."""
-        return self.trade_type == TradeType.Position
+    @classmethod
+    def from_ts(cls: tp.Type[TradesT], ts: tp.ArrayLike,
+                wrapper_kwargs: tp.KwargsLike = None, **kwargs) -> TradesT:
+        raise NotImplementedError
 
     @classmethod
     def from_orders(cls: tp.Type[TradesT], orders: Orders, **kwargs) -> TradesT:
         """Build `Trades` from `vectorbt.portfolio.orders.Orders`."""
-        trade_records_arr = nb.orders_to_trades_nb(
-            to_2d_array(orders.close),
+        trade_records_arr = nb.trades_from_orders_nb(
             orders.values,
+            to_2d_array(orders.close),
             orders.col_mapper.col_map
         )
-        return cls(orders.wrapper, trade_records_arr, orders.close, **kwargs)
-
-    @property  # no need for cached
-    def records_readable(self) -> tp.Frame:
-        """Records in readable format."""
-        records_df = self.records
-        out = pd.DataFrame()
-        _id_str = 'Trade Id' if self.trade_type == TradeType.Trade else 'Position Id'
-        out[_id_str] = records_df['id']
-        out['Column'] = records_df['col'].map(lambda x: self.wrapper.columns[x])
-        out['Size'] = records_df['size']
-        out['Entry Date'] = records_df['entry_idx'].map(lambda x: self.wrapper.index[x])
-        out['Avg. Entry Price'] = records_df['entry_price']
-        out['Entry Fees'] = records_df['entry_fees']
-        out['Exit Date'] = records_df['exit_idx'].map(lambda x: self.wrapper.index[x])
-        out['Avg. Exit Price'] = records_df['exit_price']
-        out['Exit Fees'] = records_df['exit_fees']
-        out['PnL'] = records_df['pnl']
-        out['Return'] = records_df['return']
-        out['Direction'] = map_enum_values(records_df['direction'], TradeDirection)
-        out['Status'] = map_enum_values(records_df['status'], TradeStatus)
-        if self.trade_type == TradeType.Trade:
-            out['Position Id'] = records_df['position_id']
-        return out
-
-    @cached_property
-    def duration(self) -> MappedArray:
-        """Duration of each trade (in raw format)."""
-        return self.map(nb.trade_duration_map_nb)
-
-    # ############# PnL ############# #
+        return cls(orders.wrapper, trade_records_arr, close=orders.close, **kwargs)
 
     @cached_property
     def winning(self: TradesT) -> TradesT:
         """Winning trades."""
         filter_mask = self.values['pnl'] > 0.
-        return self.filter_by_mask(filter_mask)
-
-    @cached_method
-    def win_rate(self, group_by: tp.GroupByLike = None,
-                 wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Rate of winning trades."""
-        win_count = to_1d_array(self.winning.count(group_by=group_by))
-        total_count = to_1d_array(self.count(group_by=group_by))
-        wrap_kwargs = merge_dicts(dict(name_or_index='win_rate'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(win_count / total_count, group_by=group_by, **wrap_kwargs)
+        return self.apply_mask(filter_mask)
 
     @cached_property
     def losing(self: TradesT) -> TradesT:
         """Losing trades."""
         filter_mask = self.values['pnl'] < 0.
-        return self.filter_by_mask(filter_mask)
-
-    @cached_method
-    def loss_rate(self, group_by: tp.GroupByLike = None,
-                  wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Rate of losing trades."""
-        loss_count = to_1d_array(self.losing.count(group_by=group_by))
-        total_count = to_1d_array(self.count(group_by=group_by))
-        wrap_kwargs = merge_dicts(dict(name_or_index='loss_rate'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(loss_count / total_count, group_by=group_by, **wrap_kwargs)
+        return self.apply_mask(filter_mask)
 
     @cached_property
     def winning_streak(self) -> MappedArray:
-        """Winning streak at each trade in the current column."""
+        """Winning streak at each trade in the current column.
+
+        See `vectorbt.portfolio.nb.trade_winning_streak_nb`."""
         return self.apply(nb.trade_winning_streak_nb, dtype=np.int_)
 
     @cached_property
     def losing_streak(self) -> MappedArray:
-        """Losing streak at each trade in the current column."""
+        """Losing streak at each trade in the current column.
+
+        See `vectorbt.portfolio.nb.trade_losing_streak_nb`."""
         return self.apply(nb.trade_losing_streak_nb, dtype=np.int_)
 
+    @cached_method
+    def win_rate(self, group_by: tp.GroupByLike = None,
+                 wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
+        """Rate of winning trades."""
+        win_count = to_1d_array(self.winning.count(group_by=group_by))
+        total_count = to_1d_array(self.count(group_by=group_by))
+        wrap_kwargs = merge_dicts(dict(name_or_index='win_rate'), wrap_kwargs)
+        return self.wrapper.wrap_reduced(win_count / total_count, group_by=group_by, **wrap_kwargs)
+
     @cached_method
     def profit_factor(self, group_by: tp.GroupByLike = None,
                       wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
@@ -476,75 +568,11 @@ def sqn(self, group_by: tp.GroupByLike = None,
         wrap_kwargs = merge_dicts(dict(name_or_index='sqn'), wrap_kwargs)
         return self.wrapper.wrap_reduced(sqn, group_by=group_by, **wrap_kwargs)
 
-    # ############# TradeDirection ############# #
-
-    @cached_property
-    def long(self: TradesT) -> TradesT:
-        """Long trades."""
-        filter_mask = self.values['direction'] == TradeDirection.Long
-        return self.filter_by_mask(filter_mask)
-
-    @cached_method
-    def long_rate(self, group_by: tp.GroupByLike = None,
-                  wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Rate of long trades."""
-        long_count = to_1d_array(self.long.count(group_by=group_by))
-        total_count = to_1d_array(self.count(group_by=group_by))
-        wrap_kwargs = merge_dicts(dict(name_or_index='long_rate'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(long_count / total_count, group_by=group_by, **wrap_kwargs)
-
-    @cached_property
-    def short(self: TradesT) -> TradesT:
-        """Short trades."""
-        filter_mask = self.values['direction'] == TradeDirection.Short
-        return self.filter_by_mask(filter_mask)
-
-    @cached_method
-    def short_rate(self, group_by: tp.GroupByLike = None,
-                   wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Rate of short trades."""
-        short_count = to_1d_array(self.short.count(group_by=group_by))
-        total_count = to_1d_array(self.count(group_by=group_by))
-        wrap_kwargs = merge_dicts(dict(name_or_index='short_rate'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(short_count / total_count, group_by=group_by, **wrap_kwargs)
-
-    # ############# TradeStatus ############# #
-
-    @cached_property
-    def open(self: TradesT) -> TradesT:
-        """Open trades."""
-        filter_mask = self.values['status'] == TradeStatus.Open
-        return self.filter_by_mask(filter_mask)
-
-    @cached_method
-    def open_rate(self, group_by: tp.GroupByLike = None,
-                  wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Rate of open trades."""
-        open_count = to_1d_array(self.open.count(group_by=group_by))
-        total_count = to_1d_array(self.count(group_by=group_by))
-        wrap_kwargs = merge_dicts(dict(name_or_index='open_rate'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(open_count / total_count, group_by=group_by, **wrap_kwargs)
-
-    @cached_property
-    def closed(self: TradesT) -> TradesT:
-        """Closed trades."""
-        filter_mask = self.values['status'] == TradeStatus.Closed
-        return self.filter_by_mask(filter_mask)
-
-    @cached_method
-    def closed_rate(self, group_by: tp.GroupByLike = None,
-                    wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Rate of closed trades."""
-        closed_count = to_1d_array(self.closed.count(group_by=group_by))
-        total_count = to_1d_array(self.count(group_by=group_by))
-        wrap_kwargs = merge_dicts(dict(name_or_index='closed_rate'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(closed_count / total_count, group_by=group_by, **wrap_kwargs)
-
     # ############# Stats ############# #
 
     @property
     def stats_defaults(self) -> tp.Kwargs:
-        """Defaults for `Orders.stats`.
+        """Defaults for `Trades.stats`.
 
         Merges `vectorbt.generic.stats_builder.StatsBuilderMixin.stats_defaults` and
         `trades.stats` in `vectorbt._settings.settings`."""
@@ -578,18 +606,34 @@ def stats_defaults(self) -> tp.Kwargs:
                 tags='wrapper'
             ),
             first_trade_start=dict(
-                title=RepEval("'First Position Start' if self.is_positions else 'First Trade Start'"),
+                title='First Trade Start',
                 calc_func='entry_idx.nth',
                 n=0,
                 wrap_kwargs=dict(to_index=True),
-                tags=[Rep("trades_tag"), 'index']
+                tags=['trades', 'index']
             ),
             last_trade_end=dict(
-                title=RepEval("'Last Position End' if self.is_positions else 'Last Trade End'"),
+                title='Last Trade End',
                 calc_func='exit_idx.nth',
                 n=-1,
                 wrap_kwargs=dict(to_index=True),
-                tags=[Rep("trades_tag"), 'index']
+                tags=['trades', 'index']
+            ),
+            coverage=dict(
+                title='Coverage',
+                calc_func='coverage',
+                overlapping=False,
+                normalize=False,
+                apply_to_timedelta=True,
+                tags=['ranges', 'coverage']
+            ),
+            overlap_coverage=dict(
+                title='Overlap Coverage',
+                calc_func='coverage',
+                overlapping=True,
+                normalize=False,
+                apply_to_timedelta=True,
+                tags=['ranges', 'coverage']
             ),
             total_records=dict(
                 title='Total Records',
@@ -597,106 +641,99 @@ def stats_defaults(self) -> tp.Kwargs:
                 tags='records'
             ),
             total_long_trades=dict(
-                title=RepEval("'Total Long Positions' if self.is_positions else 'Total Long Trades'"),
+                title='Total Long Trades',
                 calc_func='long.count',
-                tags=[Rep("trades_tag"), 'long']
+                tags=['trades', 'long']
             ),
             total_short_trades=dict(
-                title=RepEval("'Total Short Positions' if self.is_positions else 'Total Short Trades'"),
+                title='Total Short Trades',
                 calc_func='short.count',
-                tags=[Rep("trades_tag"), 'short']
+                tags=['trades', 'short']
             ),
             total_closed_trades=dict(
-                title=RepEval("'Total Closed Positions' if self.is_positions else 'Total Closed Trades'"),
+                title='Total Closed Trades',
                 calc_func='closed.count',
-                tags=[Rep("trades_tag"), 'closed']
+                tags=['trades', 'closed']
             ),
             total_open_trades=dict(
-                title=RepEval("'Total Open Positions' if self.is_positions else 'Total Open Trades'"),
+                title='Total Open Trades',
                 calc_func='open.count',
-                tags=[Rep("trades_tag"), 'open']
+                tags=['trades', 'open']
             ),
             open_trade_pnl=dict(
-                title=RepEval("'Open Position P&L' if self.is_positions else 'Open Trade P&L'"),
+                title='Open Trade PnL',
                 calc_func='open.pnl.sum',
-                tags=[Rep("trades_tag"), 'open']
+                tags=['trades', 'open']
             ),
             win_rate=dict(
                 title='Win Rate [%]',
-                calc_func=RepEval("'win_rate' if incl_open else 'closed.win_rate'"),
+                calc_func='closed.win_rate',
                 post_calc_func=lambda self, out, settings: out * 100,
-                tags=RepEval("[trades_tag, *incl_open_tags]")
+                tags=RepEval("['trades', *incl_open_tags]")
             ),
             winning_streak=dict(
                 title='Max Win Streak',
                 calc_func=RepEval("'winning_streak.max' if incl_open else 'closed.winning_streak.max'"),
                 wrap_kwargs=dict(dtype=pd.Int64Dtype()),
-                tags=RepEval("[trades_tag, *incl_open_tags, 'streak']")
+                tags=RepEval("['trades', *incl_open_tags, 'streak']")
             ),
             losing_streak=dict(
                 title='Max Loss Streak',
                 calc_func=RepEval("'losing_streak.max' if incl_open else 'closed.losing_streak.max'"),
                 wrap_kwargs=dict(dtype=pd.Int64Dtype()),
-                tags=RepEval("[trades_tag, *incl_open_tags, 'streak']")
+                tags=RepEval("['trades', *incl_open_tags, 'streak']")
             ),
             best_trade=dict(
-                title=RepEval("'Best Position [%]' if self.is_positions else 'Best Trade [%]'"),
+                title='Best Trade [%]',
                 calc_func=RepEval("'returns.max' if incl_open else 'closed.returns.max'"),
                 post_calc_func=lambda self, out, settings: out * 100,
-                tags=RepEval("[trades_tag, *incl_open_tags]")
+                tags=RepEval("['trades', *incl_open_tags]")
             ),
             worst_trade=dict(
-                title=RepEval("'Worst Position [%]' if self.is_positions else 'Worst Trade [%]'"),
+                title='Worst Trade [%]',
                 calc_func=RepEval("'returns.min' if incl_open else 'closed.returns.min'"),
                 post_calc_func=lambda self, out, settings: out * 100,
-                tags=RepEval("[trades_tag, *incl_open_tags]")
+                tags=RepEval("['trades', *incl_open_tags]")
             ),
             avg_winning_trade=dict(
-                title=RepEval("'Avg Winning Position [%]' if self.is_positions else 'Avg Winning Trade [%]'"),
+                title='Avg Winning Trade [%]',
                 calc_func=RepEval("'winning.returns.mean' if incl_open else 'closed.winning.returns.mean'"),
                 post_calc_func=lambda self, out, settings: out * 100,
-                tags=RepEval("[trades_tag, *incl_open_tags, 'winning']")
+                tags=RepEval("['trades', *incl_open_tags, 'winning']")
             ),
             avg_losing_trade=dict(
-                title=RepEval("'Avg Losing Position [%]' if self.is_positions else 'Avg Losing Trade [%]'"),
+                title='Avg Losing Trade [%]',
                 calc_func=RepEval("'losing.returns.mean' if incl_open else 'closed.losing.returns.mean'"),
                 post_calc_func=lambda self, out, settings: out * 100,
-                tags=RepEval("[trades_tag, *incl_open_tags, 'losing']")
+                tags=RepEval("['trades', *incl_open_tags, 'losing']")
             ),
             avg_winning_trade_duration=dict(
-                title=RepEval("'Avg Winning Position Duration' if self.is_positions else 'Avg Winning Trade Duration'"),
-                calc_func=RepEval("'winning.duration.mean' if incl_open else 'closed.winning.duration.mean'"),
-                apply_to_timedelta=True,
-                tags=RepEval("[trades_tag, *incl_open_tags, 'winning', 'duration']")
+                title='Avg Winning Trade Duration',
+                calc_func=RepEval("'winning.avg_duration' if incl_open else 'closed.winning.avg_duration'"),
+                fill_wrap_kwargs=True,
+                tags=RepEval("['trades', *incl_open_tags, 'winning', 'duration']")
             ),
             avg_losing_trade_duration=dict(
-                title=RepEval("'Avg Losing Position Duration' if self.is_positions else 'Avg Losing Trade Duration'"),
-                calc_func=RepEval("'losing.duration.mean' if incl_open else 'closed.losing.duration.mean'"),
-                apply_to_timedelta=True,
-                tags=RepEval("[trades_tag, *incl_open_tags, 'losing', 'duration']")
+                title='Avg Losing Trade Duration',
+                calc_func=RepEval("'losing.avg_duration' if incl_open else 'closed.losing.avg_duration'"),
+                fill_wrap_kwargs=True,
+                tags=RepEval("['trades', *incl_open_tags, 'losing', 'duration']")
             ),
             profit_factor=dict(
                 title='Profit Factor',
                 calc_func=RepEval("'profit_factor' if incl_open else 'closed.profit_factor'"),
-                tags=RepEval("[trades_tag, *incl_open_tags]")
+                tags=RepEval("['trades', *incl_open_tags]")
             ),
             expectancy=dict(
                 title='Expectancy',
                 calc_func=RepEval("'expectancy' if incl_open else 'closed.expectancy'"),
-                tags=RepEval("[trades_tag, *incl_open_tags]")
+                tags=RepEval("['trades', *incl_open_tags]")
             ),
             sqn=dict(
                 title='SQN',
                 calc_func=RepEval("'sqn' if incl_open else 'closed.sqn'"),
-                tags=RepEval("[trades_tag, *incl_open_tags]")
-            ),
-            coverage=dict(
-                title='Coverage [%]',
-                calc_func='coverage',
-                post_calc_func=lambda self, out, settings: out * 100,
-                check_is_positions=True,
-                tags='positions'
-            ),
+                tags=RepEval("['trades', *incl_open_tags]")
+            )
         ),
         copy_kwargs=dict(copy_mode='deep')
     )
@@ -707,25 +744,25 @@ def metrics(self) -> Config:
 
     # ############# Plotting ############# #
 
-    def plot_pnl_returns(self,
-                         column: tp.Optional[tp.Label] = None,
-                         as_pct: bool = True,
-                         marker_size_range: tp.Tuple[float, float] = (7, 14),
-                         opacity_range: tp.Tuple[float, float] = (0.75, 0.9),
-                         closed_profit_trace_kwargs: tp.KwargsLike = None,
-                         closed_loss_trace_kwargs: tp.KwargsLike = None,
-                         open_trace_kwargs: tp.KwargsLike = None,
-                         hline_shape_kwargs: tp.KwargsLike = None,
-                         add_trace_kwargs: tp.KwargsLike = None,
-                         xref: str = 'x',
-                         yref: str = 'y',
-                         fig: tp.Optional[tp.BaseFigure] = None,
-                         **layout_kwargs) -> tp.BaseFigure:  # pragma: no cover
-        """Plot trade PnL.
+    def plot_pnl(self,
+                 column: tp.Optional[tp.Label] = None,
+                 pct_scale: bool = True,
+                 marker_size_range: tp.Tuple[float, float] = (7, 14),
+                 opacity_range: tp.Tuple[float, float] = (0.75, 0.9),
+                 closed_profit_trace_kwargs: tp.KwargsLike = None,
+                 closed_loss_trace_kwargs: tp.KwargsLike = None,
+                 open_trace_kwargs: tp.KwargsLike = None,
+                 hline_shape_kwargs: tp.KwargsLike = None,
+                 add_trace_kwargs: tp.KwargsLike = None,
+                 xref: str = 'x',
+                 yref: str = 'y',
+                 fig: tp.Optional[tp.BaseFigure] = None,
+                 **layout_kwargs) -> tp.BaseFigure:  # pragma: no cover
+        """Plot trade PnL and returns.
 
         Args:
             column (str): Name of the column to plot.
-            as_pct (bool): Whether to set y-axis to `Trades.returns`, otherwise to `Trades.pnl`.
+            pct_scale (bool): Whether to set y-axis to `Trades.returns`, otherwise to `Trades.pnl`.
             marker_size_range (tuple): Range of marker size.
             opacity_range (tuple): Range of marker opacity.
             closed_profit_trace_kwargs (dict): Keyword arguments passed to `plotly.graph_objects.Scatter` for "Closed - Profit" markers.
@@ -737,6 +774,22 @@ def plot_pnl_returns(self,
             yref (str): Y coordinate axis.
             fig (Figure or FigureWidget): Figure to add traces to.
             **layout_kwargs: Keyword arguments for layout.
+
+        ## Example
+
+        ```python-repl
+        >>> import pandas as pd
+        >>> from datetime import datetime, timedelta
+        >>> import vectorbt as vbt
+
+        >>> price = pd.Series([1., 2., 3., 4., 3., 2., 1.])
+        >>> price.index = [datetime(2020, 1, 1) + timedelta(days=i) for i in range(len(price))]
+        >>> orders = pd.Series([1., -0.5, -0.5, 2., -0.5, -0.5, -0.5])
+        >>> pf = vbt.Portfolio.from_orders(price, orders)
+        >>> pf.trades.plot_pnl()
+        ```
+
+        ![](/docs/img/trades_plot_pnl.svg)
         """
         from vectorbt._settings import settings
         plotting_cfg = settings['plotting']
@@ -759,21 +812,30 @@ def plot_pnl_returns(self,
 
         if fig is None:
             fig = make_figure()
-        if as_pct:
+        if pct_scale:
             _layout_kwargs = dict()
             _layout_kwargs[yaxis] = dict(tickformat='.2%')
             fig.update_layout(**_layout_kwargs)
         fig.update_layout(**layout_kwargs)
         x_domain = get_domain(xref, fig)
 
-        if len(self_col.values) > 0:
+        if self_col.count() > 0:
             # Extract information
-            _pnl_str = '%{customdata[1]:.6f}' if as_pct else '%{y}'
-            _return_str = '%{y}' if as_pct else '%{customdata[1]:.2%}'
-            exit_idx = self_col.values['exit_idx']
-            pnl = self_col.values['pnl']
-            returns = self_col.values['return']
-            status = self_col.values['status']
+            id_ = self_col.resolve_field_arr('id')
+            id_title = self_col.resolve_field_title('id')
+
+            plot_position_id = self_col.resolve_field_setting('position_id', 'plot', True)
+
+            exit_idx = self_col.resolve_map_field_to_index('exit_idx')
+            exit_idx_title = self_col.resolve_field_title('exit_idx')
+
+            pnl = self_col.resolve_field_arr('pnl')
+            pnl_title = self_col.resolve_field_title('pnl')
+
+            returns = self_col.resolve_field_arr('return')
+            return_title = self_col.resolve_field_title('return')
+
+            status = self_col.resolve_field_arr('status')
 
             neutral_mask = pnl == 0
             profit_mask = pnl > 0
@@ -789,29 +851,33 @@ def plot_pnl_returns(self,
 
             def _plot_scatter(mask: tp.Array1d, name: tp.TraceName, color: tp.Any, kwargs: tp.Kwargs) -> None:
                 if np.any(mask):
-                    if self_col.trade_type == TradeType.Trade:
+                    if plot_position_id:
+                        position_id = self_col.resolve_field_arr('position_id')
+                        position_id_title = self_col.resolve_field_title('position_id')
                         customdata = np.stack((
-                            self_col.values['id'][mask],
-                            self_col.values['position_id'][mask],
-                            pnl[mask] if as_pct else returns[mask]
+                            id_[mask],
+                            position_id[mask],
+                            pnl[mask],
+                            returns[mask]
                         ), axis=1)
-                        hovertemplate = "Trade Id: %{customdata[0]}" \
-                                        "<br>Position Id: %{customdata[1]}" \
-                                        "<br>Date: %{x}" \
-                                        f"<br>PnL: {_pnl_str}" \
-                                        f"<br>Return: {_return_str}"
+                        hovertemplate = f"{id_title}: %{{customdata[0]}}" \
+                                        f"<br>{position_id_title}: %{{customdata[1]}}" \
+                                        f"<br>{exit_idx_title}: %{{x}}" \
+                                        f"<br>{pnl_title}: %{{customdata[2]:.6f}}" \
+                                        f"<br>{return_title}: %{{customdata[3]:.2%}}"
                     else:
                         customdata = np.stack((
-                            self_col.values['id'][mask],
-                            pnl[mask] if as_pct else returns[mask]
+                            id_[mask],
+                            pnl[mask],
+                            returns[mask]
                         ), axis=1)
-                        hovertemplate = "Position Id: %{customdata[0]}" \
-                                        "<br>Date: %{x}" \
-                                        f"<br>PnL: {_pnl_str}" \
-                                        f"<br>Return: {_return_str}"
+                        hovertemplate = f"{id_title}: %{{customdata[0]}}" \
+                                        f"<br>{exit_idx_title}: %{{x}}" \
+                                        f"<br>{pnl_title}: %{{customdata[1]:.6f}}" \
+                                        f"<br>{return_title}: %{{customdata[2]:.2%}}"
                     scatter = go.Scatter(
-                        x=self_col.wrapper.index[exit_idx[mask]],
-                        y=returns[mask] if as_pct else pnl[mask],
+                        x=exit_idx[mask],
+                        y=returns[mask] if pct_scale else pnl[mask],
                         mode='markers',
                         marker=dict(
                             symbol='circle',
@@ -870,25 +936,8 @@ def _plot_scatter(mask: tp.Array1d, name: tp.TraceName, color: tp.Any, kwargs: t
         ), hline_shape_kwargs))
         return fig
 
-    def plot_pnl(self, **kwargs):
-        """`Trades.plot_pnl_returns` with `as_pct` set to False.
-
-        ## Example
-
-        ```python-repl
-        >>> trades.plot_pnl()
-        ```
-
-        ![](/vectorbt/docs/img/trades_plot_pnl.svg)"""
-        return self.plot_pnl_returns(as_pct=False, **kwargs)
-
-    def plot_returns(self, **kwargs):
-        """`Trades.plot_pnl_returns` with `as_pct` set to True."""
-        return self.plot_pnl_returns(as_pct=True, **kwargs)
-
     def plot(self,
              column: tp.Optional[tp.Label] = None,
-             plot_close: bool = True,
              plot_zones: bool = True,
              close_trace_kwargs: tp.KwargsLike = None,
              entry_trace_kwargs: tp.KwargsLike = None,
@@ -907,7 +956,6 @@ def plot(self,
 
         Args:
             column (str): Name of the column to plot.
-            plot_close (bool): Whether to plot `Trades.close`.
             plot_zones (bool): Whether to plot zones.
 
                 Set to False if there are many trades within one position.
@@ -928,10 +976,18 @@ def plot(self,
         ## Example
 
         ```python-repl
-        >>> trades.plot()
+        >>> import pandas as pd
+        >>> from datetime import datetime, timedelta
+        >>> import vectorbt as vbt
+
+        >>> price = pd.Series([1., 2., 3., 4., 3., 2., 1.], name='Price')
+        >>> price.index = [datetime(2020, 1, 1) + timedelta(days=i) for i in range(len(price))]
+        >>> orders = pd.Series([1., -0.5, -0.5, 2., -0.5, -0.5, -0.5])
+        >>> pf = vbt.Portfolio.from_orders(price, orders)
+        >>> pf.trades.plot()
         ```
 
-        ![](/vectorbt/docs/img/trades_plot.svg)"""
+        ![](/docs/img/trades_plot.svg)"""
         from vectorbt._settings import settings
         plotting_cfg = settings['plotting']
 
@@ -967,89 +1023,149 @@ def plot(self,
         fig.update_layout(**layout_kwargs)
 
         # Plot close
-        if plot_close:
+        if self_col.close is not None:
             fig = self_col.close.vbt.plot(trace_kwargs=close_trace_kwargs, add_trace_kwargs=add_trace_kwargs, fig=fig)
 
-        if len(self_col.values) > 0:
+        if self_col.count() > 0:
             # Extract information
-            _id = self_col.values['id']
-            _id_str = 'Trade Id' if self.trade_type == TradeType.Trade else 'Position Id'
-            size = self_col.values['size']
-            entry_idx = self_col.values['entry_idx']
-            entry_price = self_col.values['entry_price']
-            entry_fees = self_col.values['entry_fees']
-            exit_idx = self_col.values['exit_idx']
-            exit_price = self_col.values['exit_price']
-            exit_fees = self_col.values['exit_fees']
-            pnl = self_col.values['pnl']
-            ret = self_col.values['return']
-            direction = map_enum_values(self_col.values['direction'], TradeDirection)
-            status = self_col.values['status']
-
-            def _get_duration_str(from_idx: int, to_idx: int) -> tp.Array1d:
-                if isinstance(self_col.wrapper.index, DatetimeIndexes):
-                    duration = self_col.wrapper.index[to_idx] - self_col.wrapper.index[from_idx]
-                elif self_col.wrapper.freq is not None:
-                    duration = self_col.wrapper.to_timedelta(to_idx - from_idx)
-                else:
-                    duration = to_idx - from_idx
-                return np.vectorize(str)(duration)
-
-            duration = _get_duration_str(entry_idx, exit_idx)
-
-            if len(entry_idx) > 0:
-                # Plot Entry markers
+            id_ = self_col.resolve_field_arr('id')
+            id_title = self_col.resolve_field_title('id')
+
+            plot_position_id = self_col.resolve_field_setting('position_id', 'plot', True)
+
+            size = self_col.resolve_field_arr('size')
+            size_title = self_col.resolve_field_title('size')
+
+            entry_idx = self_col.resolve_map_field_to_index('entry_idx')
+            entry_idx_title = self_col.resolve_field_title('entry_idx')
+
+            entry_price = self_col.resolve_field_arr('entry_price')
+            entry_price_title = self_col.resolve_field_title('entry_price')
+
+            entry_fees = self_col.resolve_field_arr('entry_fees')
+            entry_fees_title = self_col.resolve_field_title('entry_fees')
+
+            exit_idx = self_col.resolve_map_field_to_index('exit_idx')
+            exit_idx_title = self_col.resolve_field_title('exit_idx')
+
+            exit_price = self_col.resolve_field_arr('exit_price')
+            exit_price_title = self_col.resolve_field_title('exit_price')
+
+            exit_fees = self_col.resolve_field_arr('exit_fees')
+            exit_fees_title = self_col.resolve_field_title('exit_fees')
+
+            direction = self_col.resolve_apply_mapping_arr('direction')
+            direction_title = self_col.resolve_field_title('direction')
+
+            pnl = self_col.resolve_field_arr('pnl')
+            pnl_title = self_col.resolve_field_title('pnl')
+
+            returns = self_col.resolve_field_arr('return')
+            return_title = self_col.resolve_field_title('return')
+
+            status = self_col.resolve_field_arr('status')
+
+            duration = np.vectorize(str)(self_col.wrapper.to_timedelta(
+                self_col.duration.values, to_pd=True, silence_warnings=True))
+
+            # Plot Entry markers
+            if plot_position_id:
+                position_id = self_col.resolve_field_arr('position_id')
+                position_id_title = self_col.resolve_field_title('position_id')
+                entry_customdata = np.stack((
+                    id_,
+                    position_id,
+                    size,
+                    entry_fees,
+                    direction
+                ), axis=1)
+                entry_hovertemplate = f"{id_title}: %{{customdata[0]}}" \
+                                      f"<br>{position_id_title}: %{{customdata[1]}}" \
+                                      f"<br>{size_title}: %{{customdata[2]:.6f}}" \
+                                      f"<br>{entry_idx_title}: %{{x}}" \
+                                      f"<br>{entry_price_title}: %{{y}}" \
+                                      f"<br>{entry_fees_title}: %{{customdata[3]:.6f}}" \
+                                      f"<br>{direction_title}: %{{customdata[4]}}"
+            else:
                 entry_customdata = np.stack((
-                    _id,
+                    id_,
                     size,
                     entry_fees,
-                    direction,
-                    *((self_col.values['position_id'],)
-                      if self.trade_type == TradeType.Trade else ())
+                    direction
                 ), axis=1)
-                entry_scatter = go.Scatter(
-                    x=self_col.wrapper.index[entry_idx],
-                    y=entry_price,
-                    mode='markers',
-                    marker=dict(
-                        symbol='square',
-                        color=plotting_cfg['contrast_color_schema']['blue'],
-                        size=7,
-                        line=dict(
-                            width=1,
-                            color=adjust_lightness(plotting_cfg['contrast_color_schema']['blue'])
-                        )
-                    ),
-                    name='Entry',
-                    customdata=entry_customdata,
-                    hovertemplate=_id_str + ": %{customdata[0]}"
-                                            "<br>Date: %{x}"
-                                            "<br>Avg. Price: %{y}"
-                                            "<br>Size: %{customdata[1]:.6f}"
-                                            "<br>Fees: %{customdata[2]:.6f}"
-                                            "<br>Direction: %{customdata[3]}"
-                                  + ("<br>Position Id: %{customdata[4]}"
-                                     if self.trade_type == TradeType.Trade else '')
-                )
-                entry_scatter.update(**entry_trace_kwargs)
-                fig.add_trace(entry_scatter, **add_trace_kwargs)
+                entry_hovertemplate = f"{id_title}: %{{customdata[0]}}" \
+                                      f"<br>{size_title}: %{{customdata[1]:.6f}}" \
+                                      f"<br>{entry_idx_title}: %{{x}}" \
+                                      f"<br>{entry_price_title}: %{{y}}" \
+                                      f"<br>{entry_fees_title}: %{{customdata[2]:.6f}}" \
+                                      f"<br>{direction_title}: %{{customdata[3]}}"
+            entry_scatter = go.Scatter(
+                x=entry_idx,
+                y=entry_price,
+                mode='markers',
+                marker=dict(
+                    symbol='square',
+                    color=plotting_cfg['contrast_color_schema']['blue'],
+                    size=7,
+                    line=dict(
+                        width=1,
+                        color=adjust_lightness(plotting_cfg['contrast_color_schema']['blue'])
+                    )
+                ),
+                name='Entry',
+                customdata=entry_customdata,
+                hovertemplate=entry_hovertemplate
+            )
+            entry_scatter.update(**entry_trace_kwargs)
+            fig.add_trace(entry_scatter, **add_trace_kwargs)
 
             # Plot end markers
             def _plot_end_markers(mask: tp.Array1d, name: tp.TraceName, color: tp.Any, kwargs: tp.Kwargs) -> None:
                 if np.any(mask):
-                    customdata = np.stack((
-                        _id[mask],
-                        duration[mask],
-                        size[mask],
-                        exit_fees[mask],
-                        pnl[mask],
-                        ret[mask],
-                        direction[mask],
-                        *((self_col.values['position_id'][mask],)
-                          if self.trade_type == TradeType.Trade else ())
-                    ), axis=1)
+                    if plot_position_id:
+                        position_id = self_col.resolve_field_arr('position_id')
+                        position_id_title = self_col.resolve_field_title('position_id')
+                        exit_customdata = np.stack((
+                            id_[mask],
+                            position_id[mask],
+                            size[mask],
+                            exit_fees[mask],
+                            pnl[mask],
+                            returns[mask],
+                            direction[mask],
+                            duration[mask]
+                        ), axis=1)
+                        exit_hovertemplate = f"{id_title}: %{{customdata[0]}}" \
+                                             f"<br>{position_id_title}: %{{customdata[1]}}" \
+                                             f"<br>{size_title}: %{{customdata[2]:.6f}}" \
+                                             f"<br>{exit_idx_title}: %{{x}}" \
+                                             f"<br>{exit_price_title}: %{{y}}" \
+                                             f"<br>{exit_fees_title}: %{{customdata[3]:.6f}}" \
+                                             f"<br>{pnl_title}: %{{customdata[4]:.6f}}" \
+                                             f"<br>{return_title}: %{{customdata[5]:.2%}}" \
+                                             f"<br>{direction_title}: %{{customdata[6]}}" \
+                                             f"<br>Duration: %{{customdata[7]}}"
+                    else:
+                        exit_customdata = np.stack((
+                            id_[mask],
+                            size[mask],
+                            exit_fees[mask],
+                            pnl[mask],
+                            returns[mask],
+                            direction[mask],
+                            duration[mask]
+                        ), axis=1)
+                        exit_hovertemplate = f"{id_title}: %{{customdata[0]}}" \
+                                             f"<br>{size_title}: %{{customdata[1]:.6f}}" \
+                                             f"<br>{exit_idx_title}: %{{x}}" \
+                                             f"<br>{exit_price_title}: %{{y}}" \
+                                             f"<br>{exit_fees_title}: %{{customdata[2]:.6f}}" \
+                                             f"<br>{pnl_title}: %{{customdata[3]:.6f}}" \
+                                             f"<br>{return_title}: %{{customdata[4]:.2%}}" \
+                                             f"<br>{direction_title}: %{{customdata[5]}}" \
+                                             f"<br>Duration: %{{customdata[6]}}"
                     scatter = go.Scatter(
-                        x=self_col.wrapper.index[exit_idx[mask]],
+                        x=exit_idx[mask],
                         y=exit_price[mask],
                         mode='markers',
                         marker=dict(
@@ -1062,18 +1178,8 @@ def _plot_end_markers(mask: tp.Array1d, name: tp.TraceName, color: tp.Any, kwarg
                             )
                         ),
                         name=name,
-                        customdata=customdata,
-                        hovertemplate=_id_str + ": %{customdata[0]}"
-                                                "<br>Date: %{x}"
-                                                "<br>Duration: %{customdata[1]}"
-                                                "<br>Avg. Price: %{y}"
-                                                "<br>Size: %{customdata[2]:.6f}"
-                                                "<br>Fees: %{customdata[3]:.6f}"
-                                                "<br>PnL: %{customdata[4]:.6f}"
-                                                "<br>Return: %{customdata[5]:.2%}"
-                                                "<br>Direction: %{customdata[6]}"
-                                      + ("<br>Position Id: %{customdata[7]}"
-                                         if self.trade_type == TradeType.Trade else '')
+                        customdata=exit_customdata,
+                        hovertemplate=exit_hovertemplate
                     )
                     scatter.update(**kwargs)
                     fig.add_trace(scatter, **add_trace_kwargs)
@@ -1119,9 +1225,9 @@ def _plot_end_markers(mask: tp.Array1d, name: tp.TraceName, color: tp.Any, kwarg
                             type="rect",
                             xref=xref,
                             yref=yref,
-                            x0=self_col.wrapper.index[entry_idx[i]],
+                            x0=entry_idx[i],
                             y0=entry_price[i],
-                            x1=self_col.wrapper.index[exit_idx[i]],
+                            x1=exit_idx[i],
                             y1=exit_price[i],
                             fillcolor='green',
                             opacity=0.2,
@@ -1137,9 +1243,9 @@ def _plot_end_markers(mask: tp.Array1d, name: tp.TraceName, color: tp.Any, kwarg
                             type="rect",
                             xref=xref,
                             yref=yref,
-                            x0=self_col.wrapper.index[entry_idx[i]],
+                            x0=entry_idx[i],
                             y0=entry_price[i],
-                            x1=self_col.wrapper.index[exit_idx[i]],
+                            x1=exit_idx[i],
                             y1=exit_price[i],
                             fillcolor='red',
                             opacity=0.2,
@@ -1150,91 +1256,222 @@ def _plot_end_markers(mask: tp.Array1d, name: tp.TraceName, color: tp.Any, kwarg
         return fig
 
 
+Trades.override_field_config_doc(__pdoc__)
+Trades.override_metrics_doc(__pdoc__)
+
 # ############# Positions ############# #
 
-PositionsT = tp.TypeVar("Positions", bound="PositionsT")
+positions_field_config = Config(
+    dict(
+        dtype=position_dt,
+        settings={
+            'id': dict(
+                title='Position Id'
+            ),
+            'position_id': dict(
+                plot=False
+            )
+        }
+    ),
+    readonly=True,
+    as_attrs=False
+)
+"""_"""
+
+__pdoc__['positions_field_config'] = f"""Field config for `Positions`.
+
+```json
+{positions_field_config.to_doc()}
+```
+"""
+
+PositionsT = tp.TypeVar("PositionsT", bound="Positions")
 
 
-@add_mapped_fields(position_dt, trades_mf_config, on_conflict='ignore')
+@override_field_config(positions_field_config)
 class Positions(Trades):
-    """Extends `Trades` for working with position records.
-
-    In vectorbt, a position aggregates one or multiple trades sharing the same column
-    and position index. It has the same layout as a trade.
-
-    ## Example
-
-    Increasing position:
-    ```python-repl
-    >>> import vectorbt as vbt
-    >>> import pandas as pd
-
-    >>> vbt.Portfolio.from_orders(
-    ...     pd.Series([1., 2., 3., 4., 5.]),
-    ...     pd.Series([1., 1., 1., 1., -4.]),
-    ...     fixed_fees=1.).positions.records
-       id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
-    0   0    0   4.0          0          2.5         4.0         4         5.0
-
-       exit_fees  pnl  return  direction  status
-    0        1.0  5.0     0.5          0       1
-    ```
-
-    Decreasing position:
-    ```python-repl
-    >>> vbt.Portfolio.from_orders(
-    ...     pd.Series([1., 2., 3., 4., 5.]),
-    ...     pd.Series([4., -1., -1., -1., -1.]),
-    ...     fixed_fees=1.).positions.records
-       id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
-    0   0    0   4.0          0          1.0         1.0         4         3.5
-
-       exit_fees  pnl  return  direction  status
-    0        4.0  5.0    1.25          0       1
-    ```
-
-    Multiple positions:
-    ```python-repl
-    >>> vbt.Portfolio.from_orders(
-    ...     pd.Series([1., 2., 3., 4., 5.]),
-    ...     pd.Series([1., -2., 2., -2., 1.]),
-    ...     fixed_fees=1.).positions.records
-       id  col  size  entry_idx  entry_price  entry_fees  exit_idx  exit_price  \\
-    0   0    0   1.0          0          1.0         1.0         1         2.0
-    1   1    0   1.0          1          2.0         0.5         2         3.0
-    2   2    0   1.0          2          3.0         0.5         3         4.0
-    3   3    0   1.0          3          4.0         0.5         4         5.0
-
-       exit_fees  pnl  return  direction  status
-    0        0.5 -0.5  -0.500          0       1
-    1        0.5 -2.0  -1.000          1       1
-    2        0.5  0.0   0.000          0       1
-    3        1.0 -2.5  -0.625          1       1
-    ```
-    """
-    trade_type: tp.ClassVar[int] = TradeType.Position
-
-    def __init__(self, *args, **kwargs) -> None:
-        Trades.__init__(self, *args, **kwargs)
+    """Extends `Trades` for working with position records."""
 
-    @classmethod
-    def from_orders(cls: tp.Type[PositionsT], orders: Orders, **kwargs) -> PositionsT:
-        raise NotImplementedError
+    @property
+    def field_config(self) -> Config:
+        return self._field_config
 
     @classmethod
     def from_trades(cls: tp.Type[PositionsT], trades: Trades, **kwargs) -> PositionsT:
         """Build `Positions` from `Trades`."""
-        position_records_arr = nb.trades_to_positions_nb(trades.values, trades.col_mapper.col_map)
-        return cls(trades.wrapper, position_records_arr, trades.close, **kwargs)
+        position_records_arr = nb.positions_from_trades_nb(trades.values, trades.col_mapper.col_map)
+        return cls(trades.wrapper, position_records_arr, close=trades.close, **kwargs)
 
-    @cached_method
-    def coverage(self, group_by: tp.GroupByLike = None,
-                 wrap_kwargs: tp.KwargsLike = None) -> tp.SeriesFrame:
-        """Coverage, that is, total duration divided by the whole period."""
-        total_duration = to_1d_array(self.duration.sum(group_by=group_by))
-        total_steps = self.wrapper.grouper.get_group_lens(group_by=group_by) * self.wrapper.shape[0]
-        wrap_kwargs = merge_dicts(dict(name_or_index='coverage'), wrap_kwargs)
-        return self.wrapper.wrap_reduced(total_duration / total_steps, group_by=group_by, **wrap_kwargs)
+    @classmethod
+    def from_orders(cls: tp.Type[PositionsT], orders: Orders, **kwargs) -> PositionsT:
+        """Build `Trades` from `vectorbt.portfolio.orders.Orders` and `close`."""
+        return cls.from_trades(Trades.from_orders(orders), **kwargs)
 
+    # ############# Stats ############# #
 
-Trades.override_metrics_doc(__pdoc__)
+    @property
+    def stats_defaults(self) -> tp.Kwargs:
+        """Defaults for `Positions.stats`.
+
+        Merges `vectorbt.generic.stats_builder.StatsBuilderMixin.stats_defaults`,
+        `Trades.stats_defaults`, and `positions.stats` in `vectorbt._settings.settings`."""
+        from vectorbt._settings import settings
+        positions_stats_cfg = settings['positions']['stats']
+
+        return merge_dicts(
+            StatsBuilderMixin.stats_defaults.__get__(self),
+            Trades.stats_defaults.__get__(self),
+            positions_stats_cfg
+        )
+
+    _metrics: tp.ClassVar[Config] = Config(
+        dict(
+            start=dict(
+                title='Start',
+                calc_func=lambda self: self.wrapper.index[0],
+                agg_func=None,
+                tags='wrapper'
+            ),
+            end=dict(
+                title='End',
+                calc_func=lambda self: self.wrapper.index[-1],
+                agg_func=None,
+                tags='wrapper'
+            ),
+            period=dict(
+                title='Period',
+                calc_func=lambda self: len(self.wrapper.index),
+                apply_to_timedelta=True,
+                agg_func=None,
+                tags='wrapper'
+            ),
+            first_position_start=dict(
+                title='First Position Start',
+                calc_func='entry_idx.nth',
+                n=0,
+                wrap_kwargs=dict(to_index=True),
+                tags=['positions', 'index']
+            ),
+            last_position_end=dict(
+                title='Last Position End',
+                calc_func='exit_idx.nth',
+                n=-1,
+                wrap_kwargs=dict(to_index=True),
+                tags=['positions', 'index']
+            ),
+            coverage=dict(
+                title='Coverage [%]',
+                calc_func='coverage',
+                post_calc_func=lambda self, out, settings: out * 100,
+                tags=['ranges', 'duration']
+            ),
+            total_records=dict(
+                title='Total Records',
+                calc_func='count',
+                tags='records'
+            ),
+            total_long_positions=dict(
+                title='Total Long Positions',
+                calc_func='long.count',
+                tags=['positions', 'long']
+            ),
+            total_short_positions=dict(
+                title='Total Short Positions',
+                calc_func='short.count',
+                tags=['positions', 'short']
+            ),
+            total_closed_positions=dict(
+                title='Total Closed Positions',
+                calc_func='closed.count',
+                tags=['positions', 'closed']
+            ),
+            total_open_positions=dict(
+                title='Total Open Positions',
+                calc_func='open.count',
+                tags=['positions', 'open']
+            ),
+            open_position_pnl=dict(
+                title='Open Position PnL',
+                calc_func='open.pnl.sum',
+                tags=['positions', 'open']
+            ),
+            win_rate=dict(
+                title='Win Rate [%]',
+                calc_func='closed.win_rate',
+                post_calc_func=lambda self, out, settings: out * 100,
+                tags=RepEval("['positions', *incl_open_tags]")
+            ),
+            winning_streak=dict(
+                title='Max Win Streak',
+                calc_func=RepEval("'winning_streak.max' if incl_open else 'closed.winning_streak.max'"),
+                wrap_kwargs=dict(dtype=pd.Int64Dtype()),
+                tags=RepEval("['positions', *incl_open_tags, 'streak']")
+            ),
+            losing_streak=dict(
+                title='Max Loss Streak',
+                calc_func=RepEval("'losing_streak.max' if incl_open else 'closed.losing_streak.max'"),
+                wrap_kwargs=dict(dtype=pd.Int64Dtype()),
+                tags=RepEval("['positions', *incl_open_tags, 'streak']")
+            ),
+            best_position=dict(
+                title='Best Position [%]',
+                calc_func=RepEval("'returns.max' if incl_open else 'closed.returns.max'"),
+                post_calc_func=lambda self, out, settings: out * 100,
+                tags=RepEval("['positions', *incl_open_tags]")
+            ),
+            worst_position=dict(
+                title='Worst Position [%]',
+                calc_func=RepEval("'returns.min' if incl_open else 'closed.returns.min'"),
+                post_calc_func=lambda self, out, settings: out * 100,
+                tags=RepEval("['positions', *incl_open_tags]")
+            ),
+            avg_winning_position=dict(
+                title='Avg Winning Position [%]',
+                calc_func=RepEval("'winning.returns.mean' if incl_open else 'closed.winning.returns.mean'"),
+                post_calc_func=lambda self, out, settings: out * 100,
+                tags=RepEval("['positions', *incl_open_tags, 'winning']")
+            ),
+            avg_losing_position=dict(
+                title='Avg Losing Position [%]',
+                calc_func=RepEval("'losing.returns.mean' if incl_open else 'closed.losing.returns.mean'"),
+                post_calc_func=lambda self, out, settings: out * 100,
+                tags=RepEval("['positions', *incl_open_tags, 'losing']")
+            ),
+            avg_winning_position_duration=dict(
+                title='Avg Winning Position Duration',
+                calc_func=RepEval("'winning.avg_duration' if incl_open else 'closed.winning.avg_duration'"),
+                fill_wrap_kwargs=True,
+                tags=RepEval("['positions', *incl_open_tags, 'winning', 'duration']")
+            ),
+            avg_losing_position_duration=dict(
+                title='Avg Losing Position Duration',
+                calc_func=RepEval("'losing.avg_duration' if incl_open else 'closed.losing.avg_duration'"),
+                fill_wrap_kwargs=True,
+                tags=RepEval("['positions', *incl_open_tags, 'losing', 'duration']")
+            ),
+            profit_factor=dict(
+                title='Profit Factor',
+                calc_func=RepEval("'profit_factor' if incl_open else 'closed.profit_factor'"),
+                tags=RepEval("['positions', *incl_open_tags]")
+            ),
+            expectancy=dict(
+                title='Expectancy',
+                calc_func=RepEval("'expectancy' if incl_open else 'closed.expectancy'"),
+                tags=RepEval("['positions', *incl_open_tags]")
+            ),
+            sqn=dict(
+                title='SQN',
+                calc_func=RepEval("'sqn' if incl_open else 'closed.sqn'"),
+                tags=RepEval("['positions', *incl_open_tags]")
+            )
+        ),
+        copy_kwargs=dict(copy_mode='deep')
+    )
+
+    @property
+    def metrics(self) -> Config:
+        return self._metrics
+
+
+Positions.override_field_config_doc(__pdoc__)
+Positions.override_metrics_doc(__pdoc__)
diff --git a/vectorbt/px_accessors.py b/vectorbt/px_accessors.py
index d78f3899..2ef23c9c 100644
--- a/vectorbt/px_accessors.py
+++ b/vectorbt/px_accessors.py
@@ -79,7 +79,7 @@ class PXAccessor(BaseAccessor):
     >>> pd.Series([1, 2, 3]).vbt.px.bar()
     ```
 
-    ![](/vectorbt/docs/img/px_bar.svg)
+    ![](/docs/img/px_bar.svg)
     """
 
     def __init__(self, obj: tp.SeriesFrame, **kwargs) -> None:
diff --git a/vectorbt/records/base.py b/vectorbt/records/base.py
index b68efc79..9415a895 100644
--- a/vectorbt/records/base.py
+++ b/vectorbt/records/base.py
@@ -56,10 +56,10 @@
 >>> import vectorbt as vbt
 
 >>> example_dt = np.dtype([
-...     ('id', np.int64),
-...     ('idx', np.int64),
-...     ('col', np.int64),
-...     ('some_field', np.float64)
+...     ('id', np.int_),
+...     ('idx', np.int_),
+...     ('col', np.int_),
+...     ('some_field', np.float_)
 ... ])
 >>> records_arr = np.array([
 ...     (0, 0, 0, 10.),
@@ -147,11 +147,11 @@
 
 ## Filtering
 
-Use `Records.filter_by_mask` to filter elements per column/group:
+Use `Records.apply_mask` to filter elements per column/group:
 
 ```python-repl
 >>> mask = [True, False, True, False, True, False, True, False, True]
->>> filtered_records = records.filter_by_mask(mask)
+>>> filtered_records = records.apply_mask(mask)
 >>> filtered_records.count()
 a    2
 b    1
@@ -283,15 +283,91 @@
 Total Records                  6
 Name: first, dtype: object
 ```
+
+## Extending
+
+`Records` class can be extended by subclassing.
+
+In case some of our fields have the same meaning but different naming (such as the base field `idx`)
+or other properties, we can override `field_config` using `vectorbt.records.decorators.override_field_config`.
+It will look for configs of all base classes and merge our config on top of them. This preserves
+any base class property that is not explicitly listed in our config.
+
+```python-repl
+>>> import numpy as np
+>>> import pandas as pd
+>>> import vectorbt as vbt
+>>> from vectorbt.records.decorators import override_field_config
+
+>>> my_dt = np.dtype([
+...     ('my_id', np.int_),
+...     ('my_idx', np.int_),
+...     ('my_col', np.int_)
+... ])
+
+>>> my_fields_config = dict(
+...     dtype=my_dt,
+...     settings=dict(
+...         id=dict(name='my_id'),
+...         idx=dict(name='my_idx'),
+...         col=dict(name='my_col')
+...     )
+... )
+>>> @override_field_config(my_fields_config)
+... class MyRecords(vbt.Records):
+...     pass
+
+>>> records_arr = np.array([
+...     (0, 0, 0),
+...     (1, 1, 0),
+...     (2, 0, 1),
+...     (3, 1, 1)
+... ], dtype=my_dt)
+>>> wrapper = vbt.ArrayWrapper(index=['x', 'y'],
+...     columns=['a', 'b'], ndim=2, freq='1 day')
+>>> my_records = MyRecords(wrapper, records_arr)
+
+>>> my_records.id_arr
+array([0, 1, 2, 3])
+
+>>> my_records.col_arr
+array([0, 0, 1, 1])
+
+>>> my_records.idx_arr
+array([0, 1, 0, 1])
+```
+
+Alternatively, we can override the `_field_config` class attribute.
+
+```python-repl
+>>> @override_field_config
+... class MyRecords(vbt.Records):
+...     _field_config = dict(
+...         dtype=my_dt,
+...         settings=dict(
+...             id=dict(name='my_id'),
+...             idx=dict(name='my_idx'),
+...             col=dict(name='my_col')
+...         )
+...     )
+```
+
+!!! note
+    Don't forget to decorate the class with `@override_field_config` to inherit configs from base classes.
+
+    You can stop inheritance by not decorating or passing `merge_configs=False` to the decorator.
 """
 
 import numpy as np
 import pandas as pd
+import inspect
+import string
 
 from vectorbt import _typing as tp
 from vectorbt.utils import checks
 from vectorbt.utils.decorators import cached_method
 from vectorbt.utils.config import merge_dicts, Config, Configured
+from vectorbt.utils.attr import get_dict_attr
 from vectorbt.base.reshape_fns import to_1d_array
 from vectorbt.base.array_wrapper import ArrayWrapper, Wrapping
 from vectorbt.generic.stats_builder import StatsBuilderMixin
@@ -299,13 +375,42 @@
 from vectorbt.records.mapped_array import MappedArray
 from vectorbt.records.col_mapper import ColumnMapper
 
+__pdoc__ = {}
+
 RecordsT = tp.TypeVar("RecordsT", bound="Records")
 IndexingMetaT = tp.Tuple[ArrayWrapper, tp.RecordArray, tp.MaybeArray, tp.Array1d]
 
 
-class Records(Wrapping, StatsBuilderMixin):
+class MetaFields(type):
+    """Meta class that exposes a read-only class property `MetaFields.field_config`."""
+
+    @property
+    def field_config(cls) -> Config:
+        """Field config."""
+        return cls._field_config
+
+
+class RecordsWithFields(metaclass=MetaFields):
+    """Class exposes a read-only class property `RecordsWithFields.field_config`."""
+
+    @property
+    def field_config(self) -> Config:
+        """Field config of `${cls_name}`.
+
+        ```json
+        ${field_config}
+        ```
+        """
+        return self._field_config
+
+
+class MetaRecords(type(StatsBuilderMixin), type(RecordsWithFields)):
+    pass
+
+
+class Records(Wrapping, StatsBuilderMixin, RecordsWithFields, metaclass=MetaRecords):
     """Wraps the actual records array (such as trades) and exposes methods for mapping
-    it to some array of values (such as P&L of each trade).
+    it to some array of values (such as PnL of each trade).
 
     Args:
         wrapper (ArrayWrapper): Array wrapper.
@@ -314,10 +419,6 @@ class Records(Wrapping, StatsBuilderMixin):
         records_arr (array_like): A structured NumPy array of records.
 
             Must have the fields `id` (record index) and `col` (column index).
-        idx_field (str): The name of the field corresponding to the index. Optional.
-
-            Searches for a field with name 'idx' if `idx_field` is 'auto'.
-            Throws an error if the name was provided explicitly and the field cannot be found.
         col_mapper (ColumnMapper): Column mapper if already known.
 
             !!! note
@@ -329,36 +430,72 @@ class Records(Wrapping, StatsBuilderMixin):
 
             Useful if any subclass wants to extend the config.
     """
+
+    _field_config: tp.ClassVar[Config] = Config(
+        dict(
+            dtype=None,
+            settings=dict(
+                id=dict(
+                    name='id',
+                    title='Id'
+                ),
+                idx=dict(
+                    name='idx',
+                    title='Date',
+                    mapping='index'
+                ),
+                col=dict(
+                    name='col',
+                    title='Column',
+                    mapping='columns'
+                )
+            )
+        ),
+        readonly=True,
+        as_attrs=False
+    )
+
+    @property
+    def field_config(self) -> Config:
+        """Field config of `${cls_name}`.
+
+        ```json
+        ${field_config}
+        ```
+        """
+        return self._field_config
+
     def __init__(self,
                  wrapper: ArrayWrapper,
                  records_arr: tp.RecordArray,
-                 idx_field: str = 'auto',
                  col_mapper: tp.Optional[ColumnMapper] = None,
                  **kwargs) -> None:
         Wrapping.__init__(
             self,
             wrapper,
             records_arr=records_arr,
-            idx_field=idx_field,
             col_mapper=col_mapper,
             **kwargs
         )
         StatsBuilderMixin.__init__(self)
 
+        # Check fields
         records_arr = np.asarray(records_arr)
         checks.assert_not_none(records_arr.dtype.fields)
-        checks.assert_in('id', records_arr.dtype.names)
-        checks.assert_in('col', records_arr.dtype.names)
-        if idx_field == 'auto':
-            if 'idx' in records_arr.dtype.names:
-                idx_field = 'idx'
-        elif idx_field is not None:
-            checks.assert_in(idx_field, records_arr.dtype.names)
+        field_names = {
+            dct.get('name', field_name)
+            for field_name, dct in self.field_config.get('settings', {}).items()
+        }
+        dtype = self.field_config.get('dtype', None)
+        if dtype is not None:
+            for field in dtype.names:
+                if field not in records_arr.dtype.names:
+                    if field not in field_names:
+                        raise TypeError(f"Field '{field}' from {dtype} cannot be found in records or config")
 
         self._records_arr = records_arr
-        self._idx_field = idx_field
         if col_mapper is None:
-            col_mapper = ColumnMapper(wrapper, records_arr['col'])
+            col_mapper = ColumnMapper(wrapper, self.col_arr)
         self._col_mapper = col_mapper
 
     def copy(self: RecordsT, **kwargs) -> RecordsT:
@@ -414,11 +551,6 @@ def values(self) -> tp.RecordArray:
     def __len__(self) -> int:
         return len(self.values)
 
-    @property
-    def idx_field(self) -> str:
-        """Index field."""
-        return self._idx_field
-
     @property
     def records(self) -> tp.Frame:
         """Records."""
@@ -435,12 +567,82 @@ def col_mapper(self) -> ColumnMapper:
         See `vectorbt.records.col_mapper.ColumnMapper`."""
         return self._col_mapper
 
+    @property
+    def records_readable(self) -> tp.Frame:
+        """Records in readable format."""
+        df = self.records.copy()
+        field_settings = self.field_config.get('settings', {})
+        for col_name in df.columns:
+            if col_name in field_settings:
+                dct = field_settings[col_name]
+                field_name = dct.get('name', col_name)
+                if 'title' in dct:
+                    title = dct['title']
+                    new_columns = dict()
+                    new_columns[field_name] = title
+                    df.rename(columns=new_columns, inplace=True)
+                else:
+                    title = field_name
+                if 'mapping' in dct:
+                    df[title] = self.map_field(col_name).apply_mapping(dct['mapping']).values
+        return df
+
+    def resolve_field_setting(self, field: str, setting: str, default: tp.Any = None) -> tp.Any:
+        """Resolve any setting of the field. Uses `Records.field_config`."""
+        return self.field_config.get('settings', {}).get(field, {}).get(setting, default)
+
+    def resolve_field_name(self, field: str) -> str:
+        """Resolve the name of the field. Uses `Records.field_config`.."""
+        return self.resolve_field_setting(field, 'name', field)
+
+    def resolve_field_title(self, field: str) -> str:
+        """Resolve the title of the field. Uses `Records.field_config`."""
+        return self.resolve_field_setting(field, 'title', field)
+
+    def resolve_field_mapping(self, field: str) -> tp.Optional[tp.MappingLike]:
+        """Resolve the mapping of the field. Uses `Records.field_config`."""
+        return self.resolve_field_setting(field, 'mapping', None)
+
+    def resolve_field_arr(self, field: str) -> tp.Array1d:
+        """Resolve the array of the field. Uses `Records.field_config`."""
+        return self.values[self.resolve_field_name(field)]
+
+    def resolve_map_field(self, field: str, **kwargs) -> MappedArray:
+        """Resolve the mapped array of the field. Uses `Records.field_config`."""
+        return self.map_field(self.resolve_field_name(field), mapping=self.resolve_field_mapping(field), **kwargs)
+
+    def resolve_apply_mapping_arr(self, field: str, **kwargs) -> tp.Array1d:
+        """Resolve the mapped array on the field, with mapping applied. Uses `Records.field_config`."""
+        return self.resolve_map_field(field, **kwargs).apply_mapping().values
+
+    def resolve_map_field_to_index(self, field: str, **kwargs) -> tp.Index:
+        """Resolve the mapped array on the field, with index applied. Uses `Records.field_config`."""
+        return self.resolve_map_field(field, **kwargs).to_index()
+
+    @property
+    def id_arr(self) -> tp.Array1d:
+        """Get id array."""
+        return self.values[self.resolve_field_name('id')]
+
+    @property
+    def col_arr(self) -> tp.Array1d:
+        """Get column array."""
+        return self.values[self.resolve_field_name('col')]
+
+    @property
+    def idx_arr(self) -> tp.Optional[tp.Array1d]:
+        """Get index array."""
+        idx_field_name = self.resolve_field_name('idx')
+        if idx_field_name is None:
+            return None
+        return self.values[idx_field_name]
+
     @cached_method
     def is_sorted(self, incl_id: bool = False) -> bool:
         """Check whether records are sorted."""
         if incl_id:
-            return nb.is_col_idx_sorted_nb(self.values['col'], self.values['id'])
-        return nb.is_col_sorted_nb(self.values['col'])
+            return nb.is_col_idx_sorted_nb(self.col_arr, self.id_arr)
+        return nb.is_col_sorted_nb(self.col_arr)
 
     def sort(self: RecordsT, incl_id: bool = False, group_by: tp.GroupByLike = None, **kwargs) -> RecordsT:
         """Sort records by columns (primary) and ids (secondary, optional).
@@ -450,18 +652,18 @@ def sort(self: RecordsT, incl_id: bool = False, group_by: tp.GroupByLike = None,
         if self.is_sorted(incl_id=incl_id):
             return self.copy(**kwargs).regroup(group_by)
         if incl_id:
-            ind = np.lexsort((self.values['id'], self.values['col']))  # expensive!
+            ind = np.lexsort((self.id_arr, self.col_arr))  # expensive!
         else:
-            ind = np.argsort(self.values['col'])
+            ind = np.argsort(self.col_arr)
         return self.copy(records_arr=self.values[ind], **kwargs).regroup(group_by)
 
-    def filter_by_mask(self: RecordsT, mask: tp.Array1d, group_by: tp.GroupByLike = None, **kwargs) -> RecordsT:
+    def apply_mask(self: RecordsT, mask: tp.Array1d, group_by: tp.GroupByLike = None, **kwargs) -> RecordsT:
         """Return a new class instance, filtered by mask."""
         return self.copy(records_arr=self.values[mask], **kwargs).regroup(group_by)
 
     def map_array(self,
                   a: tp.ArrayLike,
-                  idx_field: tp.Optional[str] = None,
+                  idx_arr: tp.Optional[tp.ArrayLike] = None,
                   mapping: tp.Optional[tp.MappingLike] = None,
                   group_by: tp.GroupByLike = None,
                   **kwargs) -> MappedArray:
@@ -471,17 +673,13 @@ def map_array(self,
         if not isinstance(a, np.ndarray):
             a = np.asarray(a)
         checks.assert_shape_equal(a, self.values)
-        if idx_field is None:
-            idx_field = self.idx_field
-        if idx_field is not None:
-            idx_arr = self.values[idx_field]
-        else:
-            idx_arr = None
+        if idx_arr is None:
+            idx_arr = self.idx_arr
         return MappedArray(
             self.wrapper,
             a,
-            self.values['col'],
-            id_arr=self.values['id'],
+            self.col_arr,
+            id_arr=self.id_arr,
             idx_arr=idx_arr,
             mapping=mapping,
             col_mapper=self.col_mapper,
@@ -589,6 +787,24 @@ def stats_defaults(self) -> tp.Kwargs:
     def metrics(self) -> Config:
         return self._metrics
 
+    # ############# Docs ############# #
+
+    @classmethod
+    def build_field_config_doc(cls, source_cls: tp.Optional[type] = None) -> str:
+        """Build field config documentation."""
+        if source_cls is None:
+            source_cls = Records
+        return string.Template(
+            inspect.cleandoc(get_dict_attr(source_cls, 'field_config').__doc__)
+        ).substitute(
+            {'field_config': cls.field_config.to_doc(), 'cls_name': cls.__name__}
+        )
+
+    @classmethod
+    def override_field_config_doc(cls, __pdoc__: dict, source_cls: tp.Optional[type] = None) -> None:
+        """Call this method on each subclass that overrides `field_config`."""
+        __pdoc__[cls.__name__ + '.field_config'] = cls.build_field_config_doc(source_cls=source_cls)
+
 
-__pdoc__ = dict()
+Records.override_field_config_doc(__pdoc__)
 Records.override_metrics_doc(__pdoc__)
diff --git a/vectorbt/records/decorators.py b/vectorbt/records/decorators.py
index 191047bb..4f32b7bd 100644
--- a/vectorbt/records/decorators.py
+++ b/vectorbt/records/decorators.py
@@ -1,17 +1,21 @@
 """Class and function decorators."""
 
 import numpy as np
+from functools import partial
+import re
+import keyword
 
 from vectorbt import _typing as tp
 from vectorbt.utils import checks
 from vectorbt.utils.config import merge_dicts, Config
 from vectorbt.utils.decorators import cached_property
+from vectorbt.utils.mapping import to_mapping
 from vectorbt.records.mapped_array import MappedArray
 
 WrapperFuncT = tp.Callable[[tp.Type[tp.T]], tp.Type[tp.T]]
 
 
-def add_mapped_fields(dtype: np.dtype, config: tp.Optional[Config], on_conflict: str = 'raise') -> WrapperFuncT:
+def add_mapped_fields(dtype: np.dtype, config: tp.Optional[Config] = None, on_conflict: str = 'raise') -> WrapperFuncT:
     """Class decorator to override mapped field properties in a `vectorbt.records.base.Records` class.
 
     `config` should contain fields (keys) and dictionaries (values) with the following keys:
@@ -27,11 +31,13 @@ def add_mapped_fields(dtype: np.dtype, config: tp.Optional[Config], on_conflict:
     * it will be ignored if `on_conflict` is 'ignore'
     * an error will be raised if `on_conflict` is 'raise'
     """
+    if config is None:
+        config = {}
 
     def wrapper(cls: tp.Type[tp.T]) -> tp.Type[tp.T]:
         from vectorbt.records.base import Records
 
-        checks.assert_subclass(cls, Records)
+        checks.assert_subclass_of(cls, Records)
         checks.assert_not_none(dtype.fields)
 
         for field_name in dtype.names:
@@ -63,3 +69,187 @@ def new_prop(self, _field_name: str = field_name, _defaults: tp.KwargsLike = def
         return cls
 
     return wrapper
+
+
+def override_field_config(*args, merge_configs: bool = True) -> tp.Union[WrapperFuncT, tp.Type[tp.T]]:
+    """Class decorator to override field configs of all base classes in MRO that subclass
+    `vectorbt.records.base.Records`.
+    
+    Instead of overriding `_field_config` class attribute, you can pass `config` directly to this decorator.
+
+    Disable `merge_configs` to not merge, which will effectively disable field inheritance."""
+
+    def wrapper(cls: tp.Type[tp.T], config: tp.DictLike = None) -> tp.Type[tp.T]:
+        checks.assert_subclass_of(cls, "Records")
+
+        if config is None:
+            config = cls.field_config
+        if not isinstance(config, Config):
+            config = Config(config, readonly=True, as_attrs=False)
+        if merge_configs:
+            configs = []
+            for base_cls in cls.mro()[::-1]:
+                if base_cls is not cls:
+                    if checks.is_subclass_of(base_cls, "Records"):
+                        configs.append(base_cls.field_config)
+            configs.append(config)
+            config = merge_dicts(*configs, to_dict=False)
+
+        setattr(cls, "_field_config", config)
+        return cls
+
+    if len(args) == 0:
+        return wrapper
+    elif len(args) == 1:
+        if isinstance(args[0], type):
+            return wrapper(args[0])
+        return partial(wrapper, config=args[0])
+    elif len(args) == 2:
+        return wrapper(args[0], config=args[1])
+    raise ValueError("Either class, config, class and config, or keyword arguments must be passed")
+
+
+def attach_fields(*args, on_conflict: str = 'raise') -> tp.Union[WrapperFuncT, tp.Type[tp.T]]:
+    """Class decorator to attach field properties in a `vectorbt.records.base.Records` class.
+
+    Will extract `dtype` and other relevant information from `vectorbt.records.base.Records.field_config`
+    and map its fields as properties. This behavior can be changed by using `config`.
+
+    !!! note
+        Make sure to run `attach_fields` after `override_field_config`.
+
+    `config` should contain fields (keys) and dictionaries (values) with the following keys:
+
+    * `attach`: Whether to attach the field property. Can be provided as a string to be used
+        as a target attribute name. Defaults to True.
+    * `defaults`: Dictionary with default keyword arguments for `vectorbt.records.base.Records.map_field`.
+        Defaults to an empty dict.
+    * `attach_filters`: Whether to attach filters based on the field's values. Can be provided as a dict
+        to be used instead of the mapping (filter value -> target filter name). Defaults to False.
+        If True, defaults to `mapping` in `vectorbt.records.base.Records.field_config`.
+    * `filter_defaults`: Dictionary with default keyword arguments for `vectorbt.records.base.Records.apply_mask`.
+        Can be provided by target filter name. Defaults to an empty dict.
+    * `on_conflict`: Overrides global `on_conflict` for both field and filter properties.
+
+    Any potential attribute name is prepared by placing underscores between capital letters and
+    converting to the lower case.
+
+    If an attribute with the same name already exists in the class but the name is not listed in the field config:
+
+    * it will be overridden if `on_conflict` is 'override'
+    * it will be ignored if `on_conflict` is 'ignore'
+    * an error will be raised if `on_conflict` is 'raise'
+    """
+
+    def wrapper(cls: tp.Type[tp.T], config: tp.DictLike = None) -> tp.Type[tp.T]:
+        checks.assert_subclass_of(cls, "Records")
+
+        dtype = cls.field_config.get('dtype', None)
+        checks.assert_not_none(dtype.fields)
+
+        if config is None:
+            config = {}
+
+        def _prepare_attr_name(attr_name: str) -> str:
+            checks.assert_instance_of(attr_name, str)
+            attr_name = attr_name.replace('NaN', 'Nan')
+            startswith_ = attr_name.startswith('_')
+            attr_name = re.sub(r"([A-Z])", r"_\1", attr_name)
+            if not startswith_ and attr_name.startswith('_'):
+                attr_name = attr_name[1:]
+            attr_name = attr_name.lower()
+            if keyword.iskeyword(attr_name):
+                attr_name += '_'
+            return attr_name
+
+        def _check_attr_name(attr_name, _on_conflict: str = on_conflict) -> None:
+            if attr_name not in cls.field_config.get('settings', {}):
+                # Consider only attributes that are not listed in the field config
+                if hasattr(cls, attr_name):
+                    if _on_conflict.lower() == 'raise':
+                        raise ValueError(f"An attribute with the name '{attr_name}' already exists in {cls}")
+                    if _on_conflict.lower() == 'ignore':
+                        return
+                    if _on_conflict.lower() == 'override':
+                        return
+                    raise ValueError(f"Value '{_on_conflict}' is invalid for on_conflict")
+                if keyword.iskeyword(attr_name):
+                    raise ValueError(f"Name '{attr_name}' is a keyword and cannot be used as an attribute name")
+
+        if dtype is not None:
+            for field_name in dtype.names:
+                settings = config.get(field_name, {})
+                attach = settings.get('attach', True)
+                if not isinstance(attach, bool):
+                    target_name = attach
+                    attach = True
+                else:
+                    target_name = field_name
+                defaults = settings.get('defaults', None)
+                if defaults is None:
+                    defaults = {}
+                attach_filters = settings.get('attach_filters', False)
+                filter_defaults = settings.get('filter_defaults', None)
+                if filter_defaults is None:
+                    filter_defaults = {}
+                _on_conflict = settings.get('on_conflict', on_conflict)
+
+                if attach:
+                    target_name = _prepare_attr_name(target_name)
+                    _check_attr_name(target_name, _on_conflict)
+
+                    def new_prop(self,
+                                 _field_name: str = field_name,
+                                 _defaults: tp.KwargsLike = defaults) -> MappedArray:
+                        return self.resolve_map_field(_field_name, **_defaults)
+
+                    new_prop.__doc__ = f"Mapped array of the field `{field_name}`."
+                    new_prop.__name__ = target_name
+                    setattr(cls, target_name, cached_property(new_prop))
+
+                if attach_filters:
+                    if isinstance(attach_filters, bool):
+                        if not attach_filters:
+                            continue
+                        mapping = cls.field_config \
+                            .get('settings', {}) \
+                            .get(field_name, {}) \
+                            .get('mapping', None)
+                    else:
+                        mapping = attach_filters
+                    if mapping is None:
+                        raise ValueError(f"Field '{field_name}': Mapping is required to attach filters")
+                    mapping = to_mapping(mapping)
+
+                    for filter_value, target_filter_name in mapping.items():
+                        if target_filter_name is None:
+                            continue
+                        target_filter_name = _prepare_attr_name(target_filter_name)
+                        _check_attr_name(target_filter_name, _on_conflict)
+                        if target_filter_name in filter_defaults:
+                            __filter_defaults = filter_defaults[target_filter_name]
+                        else:
+                            __filter_defaults = filter_defaults
+
+                        def new_filter_prop(self,
+                                            _field_name: str = field_name,
+                                            _filter_value: tp.Any = filter_value,
+                                            _filter_defaults: tp.KwargsLike = __filter_defaults) -> MappedArray:
+                            filter_mask = self.resolve_field_arr(_field_name) == _filter_value
+                            return self.apply_mask(filter_mask, **_filter_defaults)
+
+                        new_filter_prop.__doc__ = f"Records filtered by `{field_name} == {filter_value}`."
+                        new_filter_prop.__name__ = target_filter_name
+                        setattr(cls, target_filter_name, cached_property(new_filter_prop))
+
+        return cls
+
+    if len(args) == 0:
+        return wrapper
+    elif len(args) == 1:
+        if isinstance(args[0], type):
+            return wrapper(args[0])
+        return partial(wrapper, config=args[0])
+    elif len(args) == 2:
+        return wrapper(args[0], config=args[1])
+    raise ValueError("Either class, config, class and config, or keyword arguments must be passed")
diff --git a/vectorbt/records/mapped_array.py b/vectorbt/records/mapped_array.py
index 4cfbd5ef..348dc093 100644
--- a/vectorbt/records/mapped_array.py
+++ b/vectorbt/records/mapped_array.py
@@ -126,11 +126,11 @@
 
 ## Filtering
 
-Use `MappedArray.filter_by_mask` to filter elements per column/group:
+Use `MappedArray.apply_mask` to filter elements per column/group:
 
 ```python-repl
 >>> mask = [True, False, True, False, True, False, True, False, True]
->>> filtered_ma = ma.filter_by_mask(mask)
+>>> filtered_ma = ma.apply_mask(mask)
 >>> filtered_ma.count()
 a    2
 b    1
@@ -149,7 +149,7 @@
 >>> ma.boxplot()
 ```
 
-![](/vectorbt/docs/img/mapped_boxplot.svg)
+![](/docs/img/mapped_boxplot.svg)
 
 To use scatterplots or any other plots that require index, convert to pandas first:
 
@@ -157,7 +157,7 @@
 >>> ma.to_pd().vbt.plot()
 ```
 
-![](/vectorbt/docs/img/mapped_to_pd_plot.svg)
+![](/docs/img/mapped_to_pd_plot.svg)
 
 ## Grouping
 
@@ -338,7 +338,7 @@
 
 from vectorbt import _typing as tp
 from vectorbt.utils import checks
-from vectorbt.utils.decorators import cached_method, add_binary_magic_methods, add_unary_magic_methods
+from vectorbt.utils.decorators import cached_method, attach_binary_magic_methods, attach_unary_magic_methods
 from vectorbt.utils.mapping import to_mapping, apply_mapping
 from vectorbt.utils.config import merge_dicts, Config, Configured
 from vectorbt.base.reshape_fns import to_1d_array, to_dict
@@ -373,8 +373,8 @@ def combine_mapped_with_other(self: MappedArrayT,
     return self.copy(mapped_arr=np_func(self.values, other))
 
 
-@add_binary_magic_methods(combine_mapped_with_other)
-@add_unary_magic_methods(lambda self, np_func: self.copy(mapped_arr=np_func(self.values)))
+@attach_binary_magic_methods(combine_mapped_with_other)
+@attach_unary_magic_methods(lambda self, np_func: self.copy(mapped_arr=np_func(self.values)))
 class MappedArray(Wrapping, StatsBuilderMixin):
     """Exposes methods for reducing, converting, and plotting arrays mapped by
     `vectorbt.records.base.Records` class.
@@ -439,6 +439,11 @@ def __init__(self,
             idx_arr = np.asarray(idx_arr)
             checks.assert_shape_equal(mapped_arr, idx_arr, axis=0)
         if mapping is not None:
+            if isinstance(mapping, str):
+                if mapping.lower() == 'index':
+                    mapping = self.wrapper.index
+                elif mapping.lower() == 'columns':
+                    mapping = self.wrapper.columns
             mapping = to_mapping(mapping)
 
         self._mapped_arr = mapped_arr
@@ -559,11 +564,11 @@ def sort(self: MappedArrayT,
             **kwargs
         ).regroup(group_by)
 
-    def filter_by_mask(self: MappedArrayT,
-                       mask: tp.Array1d,
-                       idx_arr: tp.Optional[tp.Array1d] = None,
-                       group_by: tp.GroupByLike = None,
-                       **kwargs) -> MappedArrayT:
+    def apply_mask(self: MappedArrayT,
+                   mask: tp.Array1d,
+                   idx_arr: tp.Optional[tp.Array1d] = None,
+                   group_by: tp.GroupByLike = None,
+                   **kwargs) -> MappedArrayT:
         """Return a new class instance, filtered by mask.
 
         `**kwargs` are passed to `MappedArray.copy`."""
@@ -598,12 +603,12 @@ def bottom_n_mask(self, n: int, **kwargs) -> tp.Array1d:
     @cached_method
     def top_n(self: MappedArrayT, n: int, **kwargs) -> MappedArrayT:
         """Filter top N elements from each column/group."""
-        return self.filter_by_mask(self.top_n_mask(n), **kwargs)
+        return self.apply_mask(self.top_n_mask(n), **kwargs)
 
     @cached_method
     def bottom_n(self: MappedArrayT, n: int, **kwargs) -> MappedArrayT:
         """Filter bottom N elements from each column/group."""
-        return self.filter_by_mask(self.bottom_n_mask(n), **kwargs)
+        return self.apply_mask(self.bottom_n_mask(n), **kwargs)
 
     @cached_method
     def is_expandable(self, idx_arr: tp.Optional[tp.Array1d] = None, group_by: tp.GroupByLike = None) -> bool:
@@ -686,7 +691,7 @@ def reduce(self,
                returns_array: bool = False,
                returns_idx: bool = False,
                to_index: bool = True,
-               fill_value: float = np.nan,
+               fill_value: tp.Scalar = np.nan,
                group_by: tp.GroupByLike = None,
                wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeriesFrame:
         """Reduce mapped array by column/group.
@@ -854,15 +859,15 @@ def std(self, ddof: int = 1, group_by: tp.GroupByLike = None,
         )
 
     @cached_method
-    def sum(self, fill_value: float = 0., group_by: tp.GroupByLike = None,
+    def sum(self, fill_value: tp.Scalar = 0., group_by: tp.GroupByLike = None,
             wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
         """Return sum by column/group."""
         wrap_kwargs = merge_dicts(dict(name_or_index='sum'), wrap_kwargs)
         return self.reduce(
             generic_nb.sum_reduce_nb,
+            fill_value=fill_value,
             returns_array=False,
             returns_idx=False,
-            fill_value=fill_value,
             group_by=group_by,
             wrap_kwargs=wrap_kwargs,
             **kwargs
@@ -897,8 +902,12 @@ def idxmax(self, group_by: tp.GroupByLike = None,
         )
 
     @cached_method
-    def describe(self, percentiles: tp.Optional[tp.ArrayLike] = None, ddof: int = 1,
-                 group_by: tp.GroupByLike = None, wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.SeriesFrame:
+    def describe(self,
+                 percentiles: tp.Optional[tp.ArrayLike] = None,
+                 ddof: int = 1,
+                 group_by: tp.GroupByLike = None,
+                 wrap_kwargs: tp.KwargsLike = None,
+                 **kwargs) -> tp.SeriesFrame:
         """Return statistics by column/group."""
         if percentiles is not None:
             percentiles = to_1d_array(percentiles)
@@ -954,6 +963,12 @@ def value_counts(self,
             Does not take into account missing values."""
         if mapping is None:
             mapping = self.mapping
+        if isinstance(mapping, str):
+            if mapping.lower() == 'index':
+                mapping = self.wrapper.index
+            elif mapping.lower() == 'columns':
+                mapping = self.wrapper.columns
+            mapping = to_mapping(mapping)
         mapped_codes, mapped_uniques = pd.factorize(self.values, sort=False, na_sentinel=None)
         col_map = self.col_mapper.get_col_map(group_by=group_by)
         value_counts = nb.mapped_value_counts_nb(mapped_codes, len(mapped_uniques), col_map)
@@ -994,6 +1009,23 @@ def value_counts(self,
             value_counts_pd.index = apply_mapping(value_counts_pd.index, mapping, **kwargs)
         return value_counts_pd
 
+    @cached_method
+    def apply_mapping(self: MappedArrayT, mapping: tp.Optional[tp.MappingLike] = None, **kwargs) -> MappedArrayT:
+        """Apply mapping on each element."""
+        if mapping is None:
+            mapping = self.mapping
+        if isinstance(mapping, str):
+            if mapping.lower() == 'index':
+                mapping = self.wrapper.index
+            elif mapping.lower() == 'columns':
+                mapping = self.wrapper.columns
+            mapping = to_mapping(mapping)
+        return self.copy(mapped_arr=apply_mapping(self.values, mapping), **kwargs)
+
+    def to_index(self):
+        """Convert to index."""
+        return self.wrapper.index[self.values]
+
     # ############# Stats ############# #
 
     @property
diff --git a/vectorbt/records/nb.py b/vectorbt/records/nb.py
index d10dc5da..762dfc3f 100644
--- a/vectorbt/records/nb.py
+++ b/vectorbt/records/nb.py
@@ -132,8 +132,8 @@ def col_map_select_nb(col_map: tp.ColMap, new_cols: tp.Array1d) -> tp.Tuple[tp.A
         col_len = col_lens[new_col]
         if col_len == 0:
             continue
-        start_idx = col_start_idxs[new_col]
-        idxs = col_idxs[start_idx:start_idx + col_len]
+        col_start_idx = col_start_idxs[new_col]
+        idxs = col_idxs[col_start_idx:col_start_idx + col_len]
         idxs_out[j:j + col_len] = idxs
         col_arr_out[j:j + col_len] = new_col_i
         j += col_len
@@ -153,9 +153,9 @@ def record_col_map_select_nb(records: tp.RecordArray, col_map: tp.ColMap, new_co
         col_len = col_lens[new_col]
         if col_len == 0:
             continue
-        start_idx = col_start_idxs[new_col]
-        idxs = col_idxs[start_idx:start_idx + col_len]
-        col_records = np.copy(records[idxs])
+        col_start_idx = col_start_idxs[new_col]
+        ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+        col_records = np.copy(records[ridxs])
         col_records['col'][:] = new_col_i
         out[j:j + col_len] = col_records
         j += col_len
@@ -205,9 +205,9 @@ def mapped_to_mask_nb(mapped_arr: tp.Array1d, col_map: tp.ColMap,
         col_len = col_lens[col]
         if col_len == 0:
             continue
-        start_idx = col_start_idxs[col]
-        idxs = col_idxs[start_idx:start_idx + col_len]
-        inout_map_func_nb(inout, idxs, col, mapped_arr[idxs], *args)
+        col_start_idx = col_start_idxs[col]
+        ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+        inout_map_func_nb(inout, ridxs, col, mapped_arr[ridxs], *args)
     return inout
 
 
@@ -241,9 +241,9 @@ def apply_on_mapped_nb(mapped_arr: tp.Array1d, col_map: tp.ColMap,
         col_len = col_lens[col]
         if col_len == 0:
             continue
-        start_idx = col_start_idxs[col]
-        idxs = col_idxs[start_idx:start_idx + col_len]
-        out[idxs] = apply_func_nb(idxs, col, mapped_arr[idxs], *args)
+        col_start_idx = col_start_idxs[col]
+        ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+        out[ridxs] = apply_func_nb(ridxs, col, mapped_arr[ridxs], *args)
     return out
 
 
@@ -263,9 +263,9 @@ def apply_on_records_nb(records: tp.RecordArray, col_map: tp.ColMap,
         col_len = col_lens[col]
         if col_len == 0:
             continue
-        start_idx = col_start_idxs[col]
-        idxs = col_idxs[start_idx:start_idx + col_len]
-        out[idxs] = apply_func_nb(records[idxs], *args)
+        col_start_idx = col_start_idxs[col]
+        ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+        out[ridxs] = apply_func_nb(records[ridxs], *args)
     return out
 
 
@@ -343,9 +343,9 @@ def _stack_expand_mapped_nb(mapped_arr, col_map, fill_value):
             col_len = col_lens[col]
             if col_len == 0:
                 continue
-            start_idx = col_start_idxs[col]
-            idxs = col_idxs[start_idx:start_idx + col_len]
-            out[:col_len, col] = mapped_arr[idxs]
+            col_start_idx = col_start_idxs[col]
+            ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+            out[:col_len, col] = mapped_arr[ridxs]
 
         return out
 
@@ -375,9 +375,9 @@ def reduce_mapped_nb(mapped_arr: tp.Array1d, col_map: tp.ColMap, fill_value: flo
         col_len = col_lens[col]
         if col_len == 0:
             continue
-        start_idx = col_start_idxs[col]
-        idxs = col_idxs[start_idx:start_idx + col_len]
-        out[col] = reduce_func_nb(col, mapped_arr[idxs], *args)
+        col_start_idx = col_start_idxs[col]
+        ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+        out[col] = reduce_func_nb(col, mapped_arr[ridxs], *args)
     return out
 
 
@@ -398,10 +398,10 @@ def reduce_mapped_to_idx_nb(mapped_arr: tp.Array1d, col_map: tp.ColMap, idx_arr:
         col_len = col_lens[col]
         if col_len == 0:
             continue
-        start_idx = col_start_idxs[col]
-        idxs = col_idxs[start_idx:start_idx + col_len]
-        col_out = reduce_func_nb(col, mapped_arr[idxs], *args)
-        out[col] = idx_arr[idxs][col_out]
+        col_start_idx = col_start_idxs[col]
+        ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+        col_out = reduce_func_nb(col, mapped_arr[ridxs], *args)
+        out[col] = idx_arr[ridxs][col_out]
     return out
 
 
@@ -416,8 +416,8 @@ def reduce_mapped_to_array_nb(mapped_arr: tp.Array1d, col_map: tp.ColMap, fill_v
     for col in range(col_lens.shape[0]):
         col_len = col_lens[col]
         if col_len > 0:
-            start_idx = col_start_idxs[col]
-            col0, idxs0 = col, col_idxs[start_idx:start_idx + col_len]
+            col_start_idx = col_start_idxs[col]
+            col0, idxs0 = col, col_idxs[col_start_idx:col_start_idx + col_len]
             break
 
     col_out = reduce_func_nb(col0, mapped_arr[idxs0], *args)
@@ -428,9 +428,9 @@ def reduce_mapped_to_array_nb(mapped_arr: tp.Array1d, col_map: tp.ColMap, fill_v
         col_len = col_lens[col]
         if col_len == 0:
             continue
-        start_idx = col_start_idxs[col]
-        idxs = col_idxs[start_idx:start_idx + col_len]
-        out[:, col] = reduce_func_nb(col, mapped_arr[idxs], *args)
+        col_start_idx = col_start_idxs[col]
+        ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+        out[:, col] = reduce_func_nb(col, mapped_arr[ridxs], *args)
     return out
 
 
@@ -448,8 +448,8 @@ def reduce_mapped_to_idx_array_nb(mapped_arr: tp.Array1d, col_map: tp.ColMap, id
     for col in range(col_lens.shape[0]):
         col_len = col_lens[col]
         if col_len > 0:
-            start_idx = col_start_idxs[col]
-            col0, idxs0 = col, col_idxs[start_idx:start_idx + col_len]
+            col_start_idx = col_start_idxs[col]
+            col0, idxs0 = col, col_idxs[col_start_idx:col_start_idx + col_len]
             break
 
     col_out = reduce_func_nb(col0, mapped_arr[idxs0], *args)
@@ -460,10 +460,10 @@ def reduce_mapped_to_idx_array_nb(mapped_arr: tp.Array1d, col_map: tp.ColMap, id
         col_len = col_lens[col]
         if col_len == 0:
             continue
-        start_idx = col_start_idxs[col]
-        idxs = col_idxs[start_idx:start_idx + col_len]
-        col_out = reduce_func_nb(col, mapped_arr[idxs], *args)
-        out[:, col] = idx_arr[idxs][col_out]
+        col_start_idx = col_start_idxs[col]
+        ridxs = col_idxs[col_start_idx:col_start_idx + col_len]
+        col_out = reduce_func_nb(col, mapped_arr[ridxs], *args)
+        out[:, col] = idx_arr[ridxs][col_out]
     return out
 
 
@@ -478,7 +478,7 @@ def mapped_value_counts_nb(codes: tp.Array1d, n_uniques: int, col_map: tp.ColMap
         col_len = col_lens[col]
         if col_len == 0:
             continue
-        start_idx = col_start_idxs[col]
+        col_start_idx = col_start_idxs[col]
         for i in range(col_len):
-            out[codes[col_idxs[start_idx + i]], col] += 1
+            out[codes[col_idxs[col_start_idx + i]], col] += 1
     return out
diff --git a/vectorbt/returns/accessors.py b/vectorbt/returns/accessors.py
index 7566857a..9a1add88 100644
--- a/vectorbt/returns/accessors.py
+++ b/vectorbt/returns/accessors.py
@@ -240,7 +240,7 @@ def defaults(self) -> tp.Kwargs:
 
     def daily(self, **kwargs) -> tp.SeriesFrame:
         """Daily returns."""
-        checks.assert_type(self.wrapper.index, DatetimeIndexes)
+        checks.assert_instance_of(self.wrapper.index, DatetimeIndexes)
 
         if self.wrapper.freq == pd.Timedelta('1D'):
             return self.obj
@@ -248,7 +248,7 @@ def daily(self, **kwargs) -> tp.SeriesFrame:
 
     def annual(self, **kwargs) -> tp.SeriesFrame:
         """Annual returns."""
-        checks.assert_type(self.obj.index, DatetimeIndexes)
+        checks.assert_instance_of(self.obj.index, DatetimeIndexes)
 
         if self.wrapper.freq == self.year_freq:
             return self.obj
@@ -781,13 +781,12 @@ def drawdowns(self) -> Drawdowns:
         """`ReturnsAccessor.get_drawdowns` with default arguments."""
         return self.get_drawdowns()
 
-    def get_drawdowns(self, group_by: tp.GroupByLike = None, **kwargs) -> Drawdowns:
+    def get_drawdowns(self, wrapper_kwargs: tp.KwargsLike = None, **kwargs) -> Drawdowns:
         """Generate drawdown records of cumulative returns.
 
         See `vectorbt.generic.drawdowns.Drawdowns`."""
-        if group_by is None:
-            group_by = self.wrapper.grouper.group_by
-        return self.cumulative(start_value=1.).vbt(freq=self.wrapper.freq, group_by=group_by).get_drawdowns(**kwargs)
+        wrapper_kwargs = merge_dicts(self.wrapper.config, wrapper_kwargs)
+        return Drawdowns.from_ts(self.cumulative(start_value=1.), wrapper_kwargs=wrapper_kwargs, **kwargs)
 
     # ############# Resolution ############# #
 
@@ -1044,7 +1043,7 @@ def plot_cumulative(self,
         >>> rets.vbt.returns.plot_cumulative(benchmark_rets=benchmark_rets)
         ```
 
-        ![](/vectorbt/docs/img/plot_cumulative.svg)
+        ![](/docs/img/plot_cumulative.svg)
         """
         from vectorbt._settings import settings
         plotting_cfg = settings['plotting']
diff --git a/vectorbt/signals/accessors.py b/vectorbt/signals/accessors.py
index 76f481c1..e2934ba4 100644
--- a/vectorbt/signals/accessors.py
+++ b/vectorbt/signals/accessors.py
@@ -122,7 +122,7 @@
 Name: a, dtype: object
 ```
 
-We can also return duration in absolute units rather than in time units:
+We can also return duration as a floating number rather than a timedelta:
 
 ```python-repl
 >>> mask.vbt.signals.stats(column='a', settings=dict(to_timedelta=False))
@@ -194,14 +194,13 @@
 from vectorbt.utils.template import RepEval
 from vectorbt.base import reshape_fns
 from vectorbt.base.array_wrapper import ArrayWrapper
+from vectorbt.records.mapped_array import MappedArray
 from vectorbt.generic.accessors import GenericAccessor, GenericSRAccessor, GenericDFAccessor
 from vectorbt.generic import plotting
 from vectorbt.generic.stats_builder import StatsBuilderMixin
+from vectorbt.generic.ranges import Ranges
 from vectorbt.generic import nb as generic_nb
 from vectorbt.signals import nb
-from vectorbt.records.mapped_array import MappedArray
-
-MaybeSeriesFrameTupleT = tp.Union[tp.SeriesFrame, tp.Tuple[tp.SeriesFrame, tp.SeriesFrame]]
 
 
 class SignalsAccessor(GenericAccessor):
@@ -424,7 +423,7 @@ def clean(cls_or_self,
               *args,
               entry_first: bool = True,
               broadcast_kwargs: tp.KwargsLike = None,
-              wrap_kwargs: tp.KwargsLike = None) -> MaybeSeriesFrameTupleT:
+              wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeTuple[tp.SeriesFrame]:
         """Clean signals.
 
         If one array passed, see `SignalsAccessor.first`.
@@ -695,7 +694,7 @@ def generate_stop_exits(self,
                             pick_first: bool = True,
                             chain: bool = False,
                             broadcast_kwargs: tp.KwargsLike = None,
-                            wrap_kwargs: tp.KwargsLike = None) -> MaybeSeriesFrameTupleT:
+                            wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeTuple[tp.SeriesFrame]:
         """Generate exits based on when `ts` hits the stop.
 
         For arguments, see `vectorbt.signals.nb.stop_choice_nb`.
@@ -799,7 +798,7 @@ def generate_ohlc_stop_exits(self,
                                  pick_first: bool = True,
                                  chain: bool = False,
                                  broadcast_kwargs: tp.KwargsLike = None,
-                                 wrap_kwargs: tp.KwargsLike = None) -> MaybeSeriesFrameTupleT:
+                                 wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeTuple[tp.SeriesFrame]:
         """Generate exits based on when the price hits (trailing) stop loss or take profit.
 
         !!! hint
@@ -1037,425 +1036,64 @@ def generate_ohlc_stop_exits(self,
                 stop_type_out, group_by=False, **merge_dicts({}, wrap_kwargs))
             return ArrayWrapper.from_obj(entries).wrap(exits, group_by=False, **merge_dicts({}, wrap_kwargs))
 
-    # ############# Map and reduce ############# #
-
-    def map_between(self,
-                    range_map_func_nb: tp.RangeMapFunc, *args,
-                    other: tp.Optional[tp.ArrayLike] = None,
-                    from_other: bool = False,
-                    use_end_idxs: tp.Optional[bool] = None,
-                    broadcast_kwargs: tp.KwargsLike = None,
-                    group_by: tp.GroupByLike = None,
-                    **kwargs) -> tp.MaybeSeries:
-        """Map using `range_map_func_nb` on the meta from `vectorbt.signals.nb.map_meta_between_nb` and
-        convert the result into an instance of `vectorbt.records.mapped_array.MappedArray`.
-
-        If `other` specified, see `vectorbt.signals.nb.map_meta_between_two_nb`.
-        Both will broadcast using `vectorbt.base.reshape_fns.broadcast` and `broadcast_kwargs`.
-
-        If `use_end_idxs` is True, uses the index of the second signal as `idx_arr`.
-        Otherwise, uses the index of the first signal. If `use_end_idxs` is None, uses the index
-        of the second signal unless `from_other` is False.
-
-        ## Example
-
-        Get average distance between signals in `mask`:
-
-        ```python-repl
-        >>> range_len_map_nb = njit(lambda from_i, to_i, col: to_i - from_i)
-
-        >>> mask.vbt.signals.map_between(range_len_map_nb).values
-        array([2., 2., 1., 1.])
-
-        >>> mask.vbt.signals.map_between(range_len_map_nb).mean()
-        a    NaN
-        b    2.0
-        c    1.0
-        Name: mean, dtype: float64
-        ```
-
-        Get all distances between signals in `mask['a']` (source) and `mask['b']` (destination):
-
-        ```python-repl
-        >>> mask['a'].vbt.signals.map_between(
-        ...     range_len_map_nb, other=mask['b']).values
-        array([0.])
-        ```
+    # ############# Ranges ############# #
 
-        The other way round:
+    def between_ranges(self,
+                       other: tp.Optional[tp.ArrayLike] = None,
+                       from_other: bool = False,
+                       broadcast_kwargs: tp.KwargsLike = None,
+                       group_by: tp.GroupByLike = None,
+                       **kwargs) -> Ranges:
+        """Wrap the result of `vectorbt.signals.nb.between_ranges_nb`
+        with `vectorbt.generic.ranges.Ranges`.
 
-        ```python-repl
-        >>> mask['a'].vbt.signals.map_between(
-        ...     range_len_map_nb, other=mask['b'], from_other=True).values
-        array([0., 2., 4.])
-        ```
-        """
+        If `other` specified, see `vectorbt.signals.nb.between_two_ranges_nb`.
+        Both will broadcast using `vectorbt.base.reshape_fns.broadcast` and `broadcast_kwargs`."""
         if broadcast_kwargs is None:
             broadcast_kwargs = {}
-        checks.assert_not_none(range_map_func_nb)
-        checks.assert_numba_func(range_map_func_nb)
 
         if other is None:
             # One input array
-            from_idxs, to_idxs, cols = nb.map_meta_between_nb(self.to_2d_array())
+            range_records = nb.between_ranges_nb(self.to_2d_array())
             wrapper = self.wrapper
         else:
             # Two input arrays
             obj, other = reshape_fns.broadcast(self.obj, other, **broadcast_kwargs)
-            from_idxs, to_idxs, cols = nb.map_meta_between_two_nb(
+            range_records = nb.between_two_ranges_nb(
                 reshape_fns.to_2d_array(obj),
                 reshape_fns.to_2d_array(other),
                 from_other=from_other
             )
             wrapper = ArrayWrapper.from_obj(obj)
-        mapped_arr = nb.range_map_meta_nb(
-            from_idxs,
-            to_idxs,
-            cols,
-            len(wrapper.columns),
-            range_map_func_nb,
-            *args
-        )
-        if use_end_idxs is None:
-            if other is not None:
-                if from_other:
-                    idx_arr = to_idxs
-                else:
-                    idx_arr = from_idxs
-            else:
-                idx_arr = to_idxs
-        else:
-            if use_end_idxs:
-                idx_arr = to_idxs
-            else:
-                idx_arr = from_idxs
-        return MappedArray(
+        return Ranges(
             wrapper,
-            mapped_arr,
-            cols,
-            idx_arr=idx_arr,
+            range_records,
             **kwargs
         ).regroup(group_by)
 
-    def map_partitions(self,
-                       range_map_func_nb: tp.RangeMapFunc, *args,
-                       use_end_idxs: bool = True,
-                       group_by: tp.GroupByLike = None,
-                       **kwargs) -> tp.MaybeSeries:
-        """Map using `range_map_func_nb` on the meta from `vectorbt.signals.nb.map_meta_partitions_nb` and
-        convert the result into an instance of `vectorbt.records.mapped_array.MappedArray`.
+    def partition_ranges(self, group_by: tp.GroupByLike = None, **kwargs) -> Ranges:
+        """Wrap the result of `vectorbt.signals.nb.partition_ranges_nb`
+        with `vectorbt.generic.ranges.Ranges`.
 
         If `use_end_idxs` is True, uses the index of the last signal in each partition as `idx_arr`.
-        Otherwise, uses the index of the first signal.
-
-        ## Example
-
-        Get average partition length in `mask`:
-
-        ```python-repl
-        >>> len_map_nb = njit(lambda from_i, to_i, col: to_i - from_i)
-
-        >>> mask.vbt.signals.map_partitions(len_map_nb).values
-        array([1., 1., 1., 1., 3.])
-
-        >>> mask.vbt.signals.map_partitions(len_map_nb).mean()
-        a    1.0
-        b    1.0
-        c    3.0
-        Name: mean, dtype: float64
-        ```
-        """
-        checks.assert_not_none(range_map_func_nb)
-        checks.assert_numba_func(range_map_func_nb)
-
-        from_idxs, to_idxs, cols = nb.map_meta_partitions_nb(self.to_2d_array())
-        mapped_arr = nb.range_map_meta_nb(
-            from_idxs,
-            to_idxs,
-            cols,
-            len(self.wrapper.columns),
-            range_map_func_nb,
-            *args
-        )
-        to_idxs -= 1
-        if use_end_idxs:
-            idx_arr = to_idxs
-        else:
-            idx_arr = from_idxs
-        return MappedArray(
+        Otherwise, uses the index of the first signal."""
+        range_records = nb.partition_ranges_nb(self.to_2d_array())
+        return Ranges(
             self.wrapper,
-            mapped_arr,
-            cols,
-            idx_arr=idx_arr,
+            range_records,
             **kwargs
         ).regroup(group_by)
 
-    def map_between_partitions(self,
-                               range_map_func_nb: tp.RangeMapFunc, *args,
-                               use_end_idxs: bool = True,
-                               group_by: tp.GroupByLike = None,
-                               **kwargs) -> tp.MaybeSeries:
-        """Map using `range_map_func_nb` on the meta from `vectorbt.signals.nb.map_meta_between_partitions_nb`
-        and convert the result into an instance of `vectorbt.records.mapped_array.MappedArray`.
-
-        If `use_end_idxs` is True, uses the index of the first signal in the second partition as `idx_arr`.
-        Otherwise, uses the index of the last signal in the first partition.
-
-        ## Example
-
-        Get average distance between partitions in `mask`:
-
-        ```python-repl
-        >>> len_map_nb = njit(lambda from_i, to_i, col: to_i - from_i)
-
-        >>> mask.vbt.signals.map_between_partitions(len_map_nb).values
-        array([2., 2.])
-
-        >>> mask.vbt.signals.map_between_partitions(len_map_nb).mean()
-        a    1.0
-        b    1.0
-        c    3.0
-        Name: mean, dtype: float64
-        ```
-        """
-        checks.assert_not_none(range_map_func_nb)
-        checks.assert_numba_func(range_map_func_nb)
-
-        from_idxs, to_idxs, cols = nb.map_meta_between_partitions_nb(self.to_2d_array())
-        mapped_arr = nb.range_map_meta_nb(
-            from_idxs,
-            to_idxs,
-            cols,
-            len(self.wrapper.columns),
-            range_map_func_nb,
-            *args
-        )
-        if use_end_idxs:
-            idx_arr = to_idxs
-        else:
-            idx_arr = from_idxs
-        return MappedArray(
+    def between_partition_ranges(self, group_by: tp.GroupByLike = None, **kwargs) -> Ranges:
+        """Wrap the result of `vectorbt.signals.nb.between_partition_ranges_nb`
+         with `vectorbt.generic.ranges.Ranges`."""
+        range_records = nb.between_partition_ranges_nb(self.to_2d_array())
+        return Ranges(
             self.wrapper,
-            mapped_arr,
-            cols,
-            idx_arr=idx_arr,
+            range_records,
             **kwargs
         ).regroup(group_by)
 
-    def distance_mapped(self, group_by: tp.GroupByLike = None, **kwargs) -> MappedArray:
-        """Get a mapped array of distance between signals.
-
-        See `SignalsAccessor.map_between`."""
-        return self.map_between(nb.range_len_map_nb, group_by=group_by, **kwargs)
-
-    def partition_len_mapped(self, group_by: tp.GroupByLike = None, **kwargs) -> MappedArray:
-        """Get a mapped array of length of partitions.
-
-        See `SignalsAccessor.map_partitions`."""
-        return self.map_partitions(nb.range_len_map_nb, group_by=group_by, **kwargs)
-
-    def partition_distance_mapped(self, group_by: tp.GroupByLike = None, **kwargs) -> MappedArray:
-        """Get a mapped array of distance between partitions.
-
-        See `SignalsAccessor.map_between_partitions`."""
-        return self.map_between_partitions(nb.range_len_map_nb, group_by=group_by, **kwargs)
-
-    def map_reduce(self,
-                   other: tp.Optional[tp.ArrayLike] = None,
-                   range_map_meta_func_nb: tp.Optional[tp.RangeMapMetaFunc] = None,
-                   range_map_meta_args: tp.ArgsLike = None,
-                   range_map_func_nb: tp.Optional[tp.RangeMapFunc] = None,
-                   range_map_args: tp.ArgsLike = None,
-                   reduce_func_nb: tp.Optional[tp.ReduceFunc] = None,
-                   reduce_args: tp.ArgsLike = None,
-                   broadcast_kwargs: tp.KwargsLike = None,
-                   wrap_kwargs: tp.KwargsLike = None) -> tp.MaybeSeries:
-        """Map ranges and reduce them per column.
-
-        The process comprises of three steps:
-
-        * Extract meta of all ranges using `range_map_meta_func_nb`
-        * Map each range to a scalar using `range_map_func_nb`
-        * Reduce all scalars in each column using `reduce_func_nb`
-
-        See `vectorbt.signals.nb.range_map_reduce_meta_nb`.
-
-        If `other` specified, will broadcast both using `vectorbt.base.reshape_fns.broadcast`
-        and `broadcast_kwargs`, and passed to `range_map_meta_func_nb`.
-        """
-        if broadcast_kwargs is None:
-            broadcast_kwargs = {}
-        checks.assert_not_none(range_map_meta_func_nb)
-        checks.assert_not_none(range_map_func_nb)
-        checks.assert_not_none(reduce_func_nb)
-        checks.assert_numba_func(range_map_meta_func_nb)
-        checks.assert_numba_func(range_map_func_nb)
-        checks.assert_numba_func(reduce_func_nb)
-        if range_map_meta_args is None:
-            range_map_meta_args = ()
-        if range_map_args is None:
-            range_map_args = ()
-        if reduce_args is None:
-            reduce_args = ()
-
-        if other is None:
-            # One input array
-            obj = self.obj
-            obj_arr = self.to_2d_array()
-            from_idxs, to_idxs, cols = range_map_meta_func_nb(obj_arr, *range_map_meta_args)
-        else:
-            # Two input arrays
-            obj, other = reshape_fns.broadcast(self.obj, other, **broadcast_kwargs)
-            checks.assert_dtype(other, np.bool_)
-            obj_arr = reshape_fns.to_2d_array(obj)
-            other_arr = reshape_fns.to_2d_array(other)
-            from_idxs, to_idxs, cols = range_map_meta_func_nb(obj_arr, other_arr, *range_map_meta_args)
-
-        result = nb.range_map_reduce_meta_nb(
-            from_idxs,
-            to_idxs,
-            cols,
-            obj_arr.shape[1],
-            range_map_func_nb, range_map_args,
-            reduce_func_nb, reduce_args
-        )
-        wrap_kwargs = merge_dicts(dict(name_or_index='map_reduce'), wrap_kwargs)
-        return ArrayWrapper.from_obj(obj).wrap_reduced(result, group_by=False, **wrap_kwargs)
-
-    def map_reduce_between(self,
-                           other: tp.Optional[tp.ArrayLike] = None,
-                           from_other: bool = False,
-                           wrap_kwargs: tp.KwargsLike = None,
-                           **kwargs) -> tp.MaybeSeries:
-        """Map-reduce all ranges between two signals.
-
-        Uses `SignalsAccessor.map_reduce` with `vectorbt.signals.nb.map_meta_between_nb` if `other` is None,
-        otherwise with `vectorbt.signals.nb.map_meta_between_two_nb`.
-
-        ## Example
-
-        The same example as in `SignalsAccessor.map_between`:
-
-        ```python-repl
-        >>> range_len_map_nb = njit(lambda from_i, to_i, col: to_i - from_i)
-        >>> mean_reduce_nb = njit(lambda col, a: np.nanmean(a))
-
-        >>> mask.vbt.signals.map_reduce_between(
-        ...     range_map_func_nb=range_len_map_nb,
-        ...     reduce_func_nb=mean_reduce_nb)
-        a    NaN
-        b    2.0
-        c    1.0
-        Name: map_reduce_between, dtype: float64
-
-        >>> mask['a'].vbt.signals.map_reduce_between(
-        ...     range_map_func_nb=range_len_map_nb,
-        ...     reduce_func_nb=mean_reduce_nb,
-        ...     other=mask['b'], from_other=True)
-        2.0
-        ```
-        """
-        if other is None:
-            wrap_kwargs = merge_dicts(dict(name_or_index='map_reduce_between'), wrap_kwargs)
-            return self.map_reduce(
-                range_map_meta_func_nb=nb.map_meta_between_nb,
-                wrap_kwargs=wrap_kwargs,
-                **kwargs
-            )
-        wrap_kwargs = merge_dicts(dict(name_or_index='map_reduce_between_two'), wrap_kwargs)
-        return self.map_reduce(
-            other=other,
-            range_map_meta_func_nb=nb.map_meta_between_two_nb,
-            range_map_meta_args=(from_other,),
-            wrap_kwargs=wrap_kwargs,
-            **kwargs
-        )
-
-    def map_reduce_partitions(self, wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
-        """Map-reduce all partitions.
-
-        Uses `SignalsAccessor.map_reduce` with `vectorbt.signals.nb.map_meta_partitions_nb`.
-
-        ## Example
-
-        The same example as in `SignalsAccessor.map_partitions`:
-
-        ```python-repl
-        >>> range_len_map_nb = njit(lambda from_i, to_i, col: to_i - from_i)
-        >>> mean_reduce_nb = njit(lambda col, a: np.nanmean(a))
-
-        >>> mask.vbt.signals.map_reduce_partitions(
-        ...     range_map_func_nb=range_len_map_nb,
-        ...     reduce_func_nb=mean_reduce_nb)
-        a    1.0
-        b    1.0
-        c    3.0
-        Name: map_reduce_partitions, dtype: float64
-        ```
-        """
-        wrap_kwargs = merge_dicts(dict(name_or_index='map_reduce_partitions'), wrap_kwargs)
-        return self.map_reduce(
-            range_map_meta_func_nb=nb.map_meta_partitions_nb,
-            wrap_kwargs=wrap_kwargs,
-            **kwargs
-        )
-
-    def map_reduce_between_partitions(self, wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
-        """Map-reduce all ranges between two partitions.
-
-        Uses `SignalsAccessor.map_reduce` with `vectorbt.signals.nb.map_meta_between_partitions_nb`.
-
-        ## Example
-
-        The same example as in `SignalsAccessor.map_partitions`:
-
-        ```python-repl
-        >>> range_len_map_nb = njit(lambda from_i, to_i, col: to_i - from_i)
-        >>> mean_reduce_nb = njit(lambda col, a: np.nanmean(a))
-
-        >>> mask.vbt.signals.map_reduce_between_partitions(
-        ...     range_map_func_nb=range_len_map_nb,
-        ...     reduce_func_nb=mean_reduce_nb)
-        a    NaN
-        b    2.0
-        c    NaN
-        Name: map_reduce_between_partitions, dtype: float64
-        ```
-        """
-        wrap_kwargs = merge_dicts(dict(name_or_index='map_reduce_between_partitions'), wrap_kwargs)
-        return self.map_reduce(
-            range_map_meta_func_nb=nb.map_meta_between_partitions_nb,
-            wrap_kwargs=wrap_kwargs,
-            **kwargs
-        )
-
-    def avg_distance(self, to: tp.Optional[tp.ArrayLike] = None,
-                     wrap_kwargs: tp.KwargsLike = None, **kwargs) -> tp.MaybeSeries:
-        """Calculate the average distance between True values in `self` and optionally `to`.
-
-        See `SignalsAccessor.distance_mapped`.
-
-        ## Example
-
-        ```python-repl
-        >>> mask.vbt.signals.avg_distance()
-        a    NaN
-        b    2.0
-        c    1.0
-        Name: avg_distance, dtype: float64
-
-        >>> mask['a'].vbt.signals.avg_distance(to=mask['b'])
-        0.0
-        ```"""
-        wrap_kwargs = merge_dicts(dict(name_or_index='avg_distance'), wrap_kwargs)
-        return self.map_reduce_between(
-            other=to,
-            range_map_func_nb=nb.range_len_map_nb,
-            reduce_func_nb=generic_nb.mean_reduce_nb,
-            wrap_kwargs=wrap_kwargs,
-            **kwargs
-        )
-
     # ############# Ranking ############# #
 
     def rank(self,
@@ -1748,23 +1386,8 @@ def rate(self, wrap_kwargs: tp.KwargsLike = None,
     def total_partitions(self, wrap_kwargs: tp.KwargsLike = None,
                          group_by: tp.GroupByLike = None, **kwargs) -> tp.MaybeSeries:
         """Total number of partitions of True values in each column/group."""
-        wrap_kwargs = merge_dicts(dict(
-            fillna=0,
-            dtype=np.int_,
-            name_or_index='total_partitions'
-        ), wrap_kwargs)
-        total_partitions = self.map_reduce_partitions(
-            range_map_func_nb=nb.range_count_map_nb,
-            reduce_func_nb=generic_nb.sum_reduce_nb,
-            wrap_kwargs=wrap_kwargs,
-            **kwargs
-        )
-        if self.is_frame() and self.wrapper.grouper.is_grouped(group_by=group_by):
-            if group_by is None:
-                group_by = self.wrapper.grouper.group_by
-            total_partitions = total_partitions.vbt.squeeze_grouped(
-                generic_nb.sum_squeeze_nb, group_by=group_by)
-        return total_partitions
+        wrap_kwargs = merge_dicts(dict(name_or_index='total_partitions'), wrap_kwargs)
+        return self.partition_ranges(**kwargs).count(group_by=group_by, wrap_kwargs=wrap_kwargs)
 
     def partition_rate(self, wrap_kwargs: tp.KwargsLike = None,
                        group_by: tp.GroupByLike = None, **kwargs) -> tp.MaybeSeries:
@@ -1899,9 +1522,7 @@ def stats_defaults(self) -> tp.Kwargs:
             distance=dict(
                 title=RepEval("f'Distance {\"<-\" if from_other else \"->\"} {other_name}' "
                               "if other is not None else 'Distance'"),
-                calc_func='distance_mapped',
-                pass_other=True,
-                pass_from_other=True,
+                calc_func='between_ranges.duration',
                 post_calc_func=lambda self, out, settings: {
                     'Min': out.min(),
                     'Max': out.max(),
@@ -1924,7 +1545,7 @@ def stats_defaults(self) -> tp.Kwargs:
             ),
             partition_len=dict(
                 title='Partition Length',
-                calc_func='partition_len_mapped',
+                calc_func='partition_ranges.duration',
                 post_calc_func=lambda self, out, settings: {
                     'Min': out.min(),
                     'Max': out.max(),
@@ -1936,7 +1557,7 @@ def stats_defaults(self) -> tp.Kwargs:
             ),
             partition_distance=dict(
                 title='Partition Distance',
-                calc_func='partition_distance_mapped',
+                calc_func='between_partition_ranges.duration',
                 post_calc_func=lambda self, out, settings: {
                     'Min': out.min(),
                     'Max': out.max(),
@@ -1969,7 +1590,7 @@ def plot(self, yref: str = 'y', **kwargs) -> tp.Union[tp.BaseFigure, plotting.Sc
         >>> mask[['a', 'c']].vbt.signals.plot()
         ```
 
-        ![](/vectorbt/docs/img/signals_df_plot.svg)
+        ![](/docs/img/signals_df_plot.svg)
         """
         default_layout = dict()
         default_layout['yaxis' + yref[1:]] = dict(
@@ -2007,7 +1628,7 @@ def plot_as_markers(self, y: tp.Optional[tp.ArrayLike] = None,
         >>> (~mask['b']).vbt.signals.plot_as_exit_markers(y=ts, fig=fig)
         ```
 
-        ![](/vectorbt/docs/img/signals_plot_as_markers.svg)
+        ![](/docs/img/signals_plot_as_markers.svg)
         """
         from vectorbt._settings import settings
         plotting_cfg = settings['plotting']
diff --git a/vectorbt/signals/enums.py b/vectorbt/signals/enums.py
index c5af40c4..17ff234b 100644
--- a/vectorbt/signals/enums.py
+++ b/vectorbt/signals/enums.py
@@ -2,6 +2,8 @@
 
 Defines enums and other schemas for `vectorbt.signals`."""
 
+import numpy as np
+
 from vectorbt import _typing as tp
 from vectorbt.utils.docs import to_doc
 
@@ -13,6 +15,9 @@
 __pdoc__ = {}
 
 
+# ############# Enums ############# #
+
+
 class StopTypeT(tp.NamedTuple):
     StopLoss: int = 0
     TrailStop: int = 1
diff --git a/vectorbt/signals/generators.py b/vectorbt/signals/generators.py
index 88aef79c..ecaeed24 100644
--- a/vectorbt/signals/generators.py
+++ b/vectorbt/signals/generators.py
@@ -677,7 +677,7 @@ def plot(self,
     >>> ohlcstx.iloc[:, 0].plot()
     ```
     
-    ![](/vectorbt/docs/img/ohlcstx.svg)
+    ![](/docs/img/OHLCSTX.svg)
     """
     return plot
 
diff --git a/vectorbt/signals/nb.py b/vectorbt/signals/nb.py
index 2c86df15..ba0b6561 100644
--- a/vectorbt/signals/nb.py
+++ b/vectorbt/signals/nb.py
@@ -28,8 +28,8 @@
 from vectorbt import _typing as tp
 from vectorbt.utils.array import uniform_summing_to_one_nb, rescale_float_to_int_nb, renormalize_nb
 from vectorbt.base.reshape_fns import flex_select_auto_nb
+from vectorbt.generic.enums import range_dt, RangeStatus
 from vectorbt.signals.enums import StopType
-from vectorbt.records import nb as records_nb
 
 
 # ############# Generation ############# #
@@ -1013,14 +1013,10 @@ def generate_ohlc_stop_enex_nb(entries: tp.Array2d,
 
 
 @njit(cache=True)
-def map_meta_between_nb(a: tp.Array2d) -> tp.RangeMapMetaOutput:
-    """Map meta of each range between two signals in `a`.
-
-    Returns three arrays: start indices (first signal), end indices (second signal), and columns."""
-    from_idxs_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    to_idxs_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    cols_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    k = 0
+def between_ranges_nb(a: tp.Array2d) -> tp.RecordArray:
+    """Create a record of type `vectorbt.generic.enums.range_dt` for each range between two signals in `a`."""
+    range_records = np.empty(a.shape[0] * a.shape[1], dtype=range_dt)
+    ridx = 0
 
     for col in range(a.shape[1]):
         a_idxs = np.flatnonzero(a[:, col])
@@ -1028,28 +1024,26 @@ def map_meta_between_nb(a: tp.Array2d) -> tp.RangeMapMetaOutput:
             for j in range(1, a_idxs.shape[0]):
                 from_i = a_idxs[j - 1]
                 to_i = a_idxs[j]
-                from_idxs_out[k] = from_i
-                to_idxs_out[k] = to_i
-                cols_out[k] = col
-                k += 1
-    return from_idxs_out[:k], to_idxs_out[:k], cols_out[:k]
+                range_records[ridx]['id'] = ridx
+                range_records[ridx]['col'] = col
+                range_records[ridx]['start_idx'] = from_i
+                range_records[ridx]['end_idx'] = to_i
+                range_records[ridx]['status'] = RangeStatus.Closed
+                ridx += 1
+    return range_records[:ridx]
 
 
 @njit(cache=True)
-def map_meta_between_two_nb(a: tp.Array2d, b: tp.Array2d, from_other: bool = False) -> tp.RangeMapMetaOutput:
-    """Map meta of each range between two signals in `a` and `b`.
+def between_two_ranges_nb(a: tp.Array2d, b: tp.Array2d, from_other: bool = False) -> tp.RecordArray:
+    """Create a record of type `vectorbt.generic.enums.range_dt` for each range between two signals in `a` and `b`.
 
     If `from_other` is False, returns ranges from each in `a` to the succeeding in `b`.
     Otherwise, returns ranges from each in `b` to the preceding in `a`.
 
     When `a` and `b` overlap (two signals at the same time), the distance between overlapping
-    signals is still considered and `from_i` would match `to_i`.
-
-    Returns three arrays: start indices (first signal), end indices (second signal), and columns."""
-    from_idxs_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    to_idxs_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    cols_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    k = 0
+    signals is still considered and `from_i` would match `to_i`."""
+    range_records = np.empty(a.shape[0] * a.shape[1], dtype=range_dt)
+    ridx = 0
 
     for col in range(a.shape[1]):
         a_idxs = np.flatnonzero(a[:, col])
@@ -1061,31 +1055,31 @@ def map_meta_between_two_nb(a: tp.Array2d, b: tp.Array2d, from_other: bool = Fal
                         valid_a_idxs = a_idxs[a_idxs <= to_i]
                         if len(valid_a_idxs) > 0:
                             from_i = valid_a_idxs[-1]  # preceding in a
-                            from_idxs_out[k] = from_i
-                            to_idxs_out[k] = to_i
-                            cols_out[k] = col
-                            k += 1
+                            range_records[ridx]['id'] = ridx
+                            range_records[ridx]['col'] = col
+                            range_records[ridx]['start_idx'] = from_i
+                            range_records[ridx]['end_idx'] = to_i
+                            range_records[ridx]['status'] = RangeStatus.Closed
+                            ridx += 1
                 else:
                     for j, from_i in enumerate(a_idxs):
                         valid_b_idxs = b_idxs[b_idxs >= from_i]
                         if len(valid_b_idxs) > 0:
                             to_i = valid_b_idxs[0]  # succeeding in b
-                            from_idxs_out[k] = from_i
-                            to_idxs_out[k] = to_i
-                            cols_out[k] = col
-                            k += 1
-    return from_idxs_out[:k], to_idxs_out[:k], cols_out[:k]
+                            range_records[ridx]['id'] = ridx
+                            range_records[ridx]['col'] = col
+                            range_records[ridx]['start_idx'] = from_i
+                            range_records[ridx]['end_idx'] = to_i
+                            range_records[ridx]['status'] = RangeStatus.Closed
+                            ridx += 1
+    return range_records[:ridx]
 
 
 @njit(cache=True)
-def map_meta_partitions_nb(a: tp.Array2d) -> tp.RangeMapMetaOutput:
-    """Map meta of each partition of signals in `a`.
-
-    Returns three arrays: start indices (first signal), end indices (second signal), and columns."""
-    from_idxs_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    to_idxs_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    cols_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    k = 0
+def partition_ranges_nb(a: tp.Array2d) -> tp.RecordArray:
+    """Create a record of type `vectorbt.generic.enums.range_dt` for each partition of signals in `a`."""
+    range_records = np.empty(a.shape[0] * a.shape[1], dtype=range_dt)
+    ridx = 0
 
     for col in range(a.shape[1]):
         is_partition = False
@@ -1097,30 +1091,30 @@ def map_meta_partitions_nb(a: tp.Array2d) -> tp.RangeMapMetaOutput:
                 is_partition = True
             elif is_partition:
                 to_i = i
-                from_idxs_out[k] = from_i
-                to_idxs_out[k] = to_i
-                cols_out[k] = col
-                k += 1
+                range_records[ridx]['id'] = ridx
+                range_records[ridx]['col'] = col
+                range_records[ridx]['start_idx'] = from_i
+                range_records[ridx]['end_idx'] = to_i
+                range_records[ridx]['status'] = RangeStatus.Closed
+                ridx += 1
                 is_partition = False
             if i == a.shape[0] - 1:
                 if is_partition:
-                    to_i = a.shape[0]
-                    from_idxs_out[k] = from_i
-                    to_idxs_out[k] = to_i
-                    cols_out[k] = col
-                    k += 1
-    return from_idxs_out[:k], to_idxs_out[:k], cols_out[:k]
+                    to_i = a.shape[0] - 1
+                    range_records[ridx]['id'] = ridx
+                    range_records[ridx]['col'] = col
+                    range_records[ridx]['start_idx'] = from_i
+                    range_records[ridx]['end_idx'] = to_i
+                    range_records[ridx]['status'] = RangeStatus.Open
+                    ridx += 1
+    return range_records[:ridx]
 
 
 @njit(cache=True)
-def map_meta_between_partitions_nb(a: tp.Array2d) -> tp.RangeMapMetaOutput:
-    """Map meta of each range between two partitions in `a`.
-
-    Returns three arrays: start indices (first signal), end indices (second signal), and columns."""
-    from_idxs_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    to_idxs_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    cols_out = np.empty(a.shape[0] * a.shape[1], dtype=np.int_)
-    k = 0
+def between_partition_ranges_nb(a: tp.Array2d) -> tp.RecordArray:
+    """Create a record of type `vectorbt.generic.enums.range_dt` for each range between two partitions in `a`."""
+    range_records = np.empty(a.shape[0] * a.shape[1], dtype=range_dt)
+    ridx = 0
 
     for col in range(a.shape[1]):
         is_partition = False
@@ -1129,81 +1123,17 @@ def map_meta_between_partitions_nb(a: tp.Array2d) -> tp.RangeMapMetaOutput:
             if a[i, col]:
                 if not is_partition and from_i != -1:
                     to_i = i
-                    from_idxs_out[k] = from_i
-                    to_idxs_out[k] = to_i
-                    cols_out[k] = col
-                    k += 1
+                    range_records[ridx]['id'] = ridx
+                    range_records[ridx]['col'] = col
+                    range_records[ridx]['start_idx'] = from_i
+                    range_records[ridx]['end_idx'] = to_i
+                    range_records[ridx]['status'] = RangeStatus.Closed
+                    ridx += 1
                 is_partition = True
                 from_i = i
             else:
                 is_partition = False
-    return from_idxs_out[:k], to_idxs_out[:k], cols_out[:k]
-
-
-@njit
-def range_map_meta_nb(from_idxs: tp.Array1d,
-                      to_idxs: tp.Array1d,
-                      cols: tp.Array2d,
-                      n_cols: int,
-                      range_map_func_nb: tp.RangeMapFunc,
-                      *args) -> tp.Array1d:
-    """Map meta of each range using `range_map_func_nb`.
-
-    Applies `range_map_func_nb` on each range `[from_i, to_i)`. Should accept index of the start of the
-    range `from_i`, index of the end of the range `to_i`, index of the column `col`, and `*range_map_args`.
-    """
-    out = np.full(cols.shape[0], np.nan, dtype=np.float_)
-    col_range = records_nb.col_range_nb(cols, n_cols)
-
-    for col in range(n_cols):
-        from_k = col_range[col, 0]
-        to_k = col_range[col, 1]
-        if from_k != -1 and to_k != -1:
-            for k in range(from_k, to_k):
-                out[k] = range_map_func_nb(from_idxs[k], to_idxs[k], col, *args)
-    return out
-
-
-@njit
-def range_map_reduce_meta_nb(from_idxs: tp.Array1d,
-                             to_idxs: tp.Array1d,
-                             cols: tp.Array2d,
-                             n_cols: int,
-                             range_map_func_nb: tp.RangeMapFunc,
-                             range_map_args: tp.Args,
-                             reduce_func_nb: tp.ReduceFunc,
-                             reduce_args: tp.Args) -> tp.Array1d:
-    """Map meta of each range range_map_func_nb` and reduce per column using `reduce_func_nb`.
-
-    `range_map_func_nb` is the same as for `map_meta`.
-
-    Applies `reduce_func_nb` on all mapper results in a column. Should accept index of the column,
-    the array of results from `range_map_func_nb` for that column, and `*reduce_args`.
-    """
-    out = np.full(n_cols, np.nan, dtype=np.float_)
-    map_res = np.empty(cols.shape[0], dtype=np.float_)
-    col_range = records_nb.col_range_nb(cols, n_cols)
-
-    for col in range(n_cols):
-        from_k = col_range[col, 0]
-        to_k = col_range[col, 1]
-        if from_k != -1 and to_k != -1:
-            for k in range(from_k, to_k):
-                map_res[k] = range_map_func_nb(from_idxs[k], to_idxs[k], col, *range_map_args)
-            out[col] = reduce_func_nb(col, map_res[from_k:to_k], *reduce_args)
-    return out
-
-
-@njit(cache=True)
-def range_len_map_nb(from_i: int, to_i: int, col: int) -> int:
-    """Range length mapper."""
-    return to_i - from_i
-
-
-@njit(cache=True)
-def range_count_map_nb(from_i: int, to_i: int, col: int) -> int:
-    """Range count mapper."""
-    return 1
+    return range_records[:ridx]
 
 
 # ############# Ranking ############# #
diff --git a/vectorbt/utils/attr.py b/vectorbt/utils/attr.py
index ff68b7c6..2381a9ab 100644
--- a/vectorbt/utils/attr.py
+++ b/vectorbt/utils/attr.py
@@ -56,7 +56,7 @@ def deep_getattr(obj: tp.Any,
         If your chain includes only attributes and functions without arguments,
         you can represent this chain as a single (but probably long) string.
     """
-    checks.assert_type(attr_chain, (str, tuple, Iterable))
+    checks.assert_instance_of(attr_chain, (str, tuple, Iterable))
 
     if isinstance(attr_chain, str):
         if '.' in attr_chain:
diff --git a/vectorbt/utils/checks.py b/vectorbt/utils/checks.py
index 966f5597..971fb307 100644
--- a/vectorbt/utils/checks.py
+++ b/vectorbt/utils/checks.py
@@ -7,6 +7,7 @@
 from inspect import signature, getmro
 import dill
 from collections.abc import Hashable, Mapping
+from keyword import iskeyword
 
 from vectorbt import _typing as tp
 
@@ -260,6 +261,11 @@ def is_mapping_like(arg: tp.Any) -> bool:
     return is_mapping(arg) or is_series(arg) or is_index(arg) or is_namedtuple(arg)
 
 
+def is_valid_variable_name(arg: str) -> bool:
+    """Check whether the argument is a valid variable name."""
+    return arg.isidentifier() and not iskeyword(arg)
+
+
 # ############# Asserts ############# #
 
 def safe_assert(arg: tp.Any, msg: tp.Optional[str] = None) -> None:
@@ -285,7 +291,7 @@ def assert_not_none(arg: tp.Any) -> None:
         raise AssertionError(f"Argument cannot be None")
 
 
-def assert_type(arg: tp.Any, types: tp.MaybeTuple[tp.Type]) -> None:
+def assert_instance_of(arg: tp.Any, types: tp.MaybeTuple[tp.Type]) -> None:
     """Raise exception if the argument is none of types `types`."""
     if not is_instance_of(arg, types):
         if isinstance(types, tuple):
@@ -294,7 +300,7 @@ def assert_type(arg: tp.Any, types: tp.MaybeTuple[tp.Type]) -> None:
             raise AssertionError(f"Type must be {types}, not {type(arg)}")
 
 
-def assert_subclass(arg: tp.Type, classes: tp.MaybeTuple[tp.Type]) -> None:
+def assert_subclass_of(arg: tp.Type, classes: tp.MaybeTuple[tp.Type]) -> None:
     """Raise exception if the argument is not a subclass of classes `classes`."""
     if not is_subclass_of(arg, classes):
         if isinstance(classes, tuple):
diff --git a/vectorbt/utils/config.py b/vectorbt/utils/config.py
index c420d32d..0c123747 100644
--- a/vectorbt/utils/config.py
+++ b/vectorbt/utils/config.py
@@ -108,7 +108,7 @@ def copy_dict(dct: InConfigLikeT, copy_mode: str = 'shallow', nested: bool = Tru
     Set `nested` to True to copy all child dicts in recursive manner."""
     if dct is None:
         dct = {}
-    checks.assert_type(copy_mode, str)
+    checks.assert_instance_of(copy_mode, str)
     copy_mode = copy_mode.lower()
     if copy_mode not in ['shallow', 'hybrid', 'deep']:
         raise ValueError(f"Copy mode '{copy_mode}' not supported")
@@ -151,8 +151,8 @@ def update_dict(x: InConfigLikeT,
         return
     if y is None:
         return
-    checks.assert_type(x, dict)
-    checks.assert_type(y, dict)
+    checks.assert_instance_of(x, dict)
+    checks.assert_instance_of(y, dict)
 
     for k, v in y.items():
         if nested \
@@ -423,14 +423,14 @@ def _resolve_param(pname: str, p: tp.Any, default: tp.Any, merge: bool = False)
         dict.__init__(self, dct)
 
         # Store params in an instance variable
-        checks.assert_type(copy_kwargs, dict)
-        checks.assert_type(reset_dct, dict)
-        checks.assert_type(reset_dct_copy_kwargs, dict)
-        checks.assert_type(frozen_keys, bool)
-        checks.assert_type(readonly, bool)
-        checks.assert_type(nested, bool)
-        checks.assert_type(convert_dicts, (bool, type))
-        checks.assert_type(as_attrs, bool)
+        checks.assert_instance_of(copy_kwargs, dict)
+        checks.assert_instance_of(reset_dct, dict)
+        checks.assert_instance_of(reset_dct_copy_kwargs, dict)
+        checks.assert_instance_of(frozen_keys, bool)
+        checks.assert_instance_of(readonly, bool)
+        checks.assert_instance_of(nested, bool)
+        checks.assert_instance_of(convert_dicts, (bool, type))
+        checks.assert_instance_of(as_attrs, bool)
 
         self.__dict__['_copy_kwargs_'] = copy_kwargs
         self.__dict__['_reset_dct_'] = reset_dct
diff --git a/vectorbt/utils/decorators.py b/vectorbt/utils/decorators.py
index 908018b3..6396c642 100644
--- a/vectorbt/utils/decorators.py
+++ b/vectorbt/utils/decorators.py
@@ -191,7 +191,7 @@ def should_cache(func_name: str, instance: object, func: tp.Optional[tp.Callable
 
     def _get_condition_rank(cond: CacheCondition) -> int:
         # Perform initial checks
-        checks.assert_type(cond, CacheCondition)
+        checks.assert_instance_of(cond, CacheCondition)
 
         if cond.instance is not None:
             if instance is not cond.instance:
@@ -499,9 +499,8 @@ def clear_cache(instance):
         '__ror__': dict(func=lambda x, y: np.bitwise_or(y, x)),
         '__rxor__': dict(func=lambda x, y: np.bitwise_xor(y, x))
     },
-    as_attrs=False,
     readonly=True,
-    copy_kwargs=dict(copy_mode='deep')
+    as_attrs=False
 )
 """_"""
 
@@ -515,7 +514,7 @@ def clear_cache(instance):
 BinaryTranslateFuncT = tp.Callable[[tp.Any, tp.Any, tp.Callable], tp.Any]
 
 
-def add_binary_magic_methods(translate_func: BinaryTranslateFuncT,
+def attach_binary_magic_methods(translate_func: BinaryTranslateFuncT,
                              config: tp.Optional[Config] = None) -> WrapperFuncT:
     """Class decorator to add binary magic methods to a class.
 
@@ -558,9 +557,8 @@ def new_method(self,
         '__abs__': dict(func=np.absolute),
         '__invert__': dict(func=np.invert)
     },
-    as_attrs=False,
     readonly=True,
-    copy_kwargs=dict(copy_mode='deep')
+    as_attrs=False
 )
 """_"""
 
@@ -574,7 +572,7 @@ def new_method(self,
 UnaryTranslateFuncT = tp.Callable[[tp.Any, tp.Callable], tp.Any]
 
 
-def add_unary_magic_methods(translate_func: UnaryTranslateFuncT,
+def attach_unary_magic_methods(translate_func: UnaryTranslateFuncT,
                             config: tp.Optional[Config] = None) -> WrapperFuncT:
     """Class decorator to add unary magic methods to a class.
 
diff --git a/vectorbt/utils/enum.py b/vectorbt/utils/enum.py
index 106ccfe0..d82d0fa2 100644
--- a/vectorbt/utils/enum.py
+++ b/vectorbt/utils/enum.py
@@ -7,19 +7,19 @@
 from vectorbt.utils.mapping import to_mapping, apply_mapping
 
 
-def map_enum_fields(field: tp.Any, enum: tp.Enum, **kwargs) -> tp.Any:
+def map_enum_fields(field: tp.Any, enum: tp.Enum, ignore_type=int, **kwargs) -> tp.Any:
     """Map fields to values.
 
     See `vectorbt.utils.mapping.apply_mapping`."""
     mapping = to_mapping(enum, reverse=True)
 
-    return apply_mapping(field, mapping, **kwargs)
+    return apply_mapping(field, mapping, ignore_type=ignore_type, **kwargs)
 
 
-def map_enum_values(value: tp.Any, enum: tp.Enum, **kwargs) -> tp.Any:
+def map_enum_values(value: tp.Any, enum: tp.Enum, ignore_type=str, **kwargs) -> tp.Any:
     """Map values to fields.
 
     See `vectorbt.utils.mapping.apply_mapping`."""
     mapping = to_mapping(enum, reverse=False)
 
-    return apply_mapping(value, mapping, **kwargs)
+    return apply_mapping(value, mapping, ignore_type=ignore_type, **kwargs)
diff --git a/vectorbt/utils/mapping.py b/vectorbt/utils/mapping.py
index a18b501b..82b8f66a 100644
--- a/vectorbt/utils/mapping.py
+++ b/vectorbt/utils/mapping.py
@@ -41,7 +41,8 @@ def apply_mapping(obj: tp.Any,
                   reverse: bool = False,
                   ignore_case: bool = True,
                   ignore_underscores: bool = True,
-                  ignore_other_types: bool = True,
+                  ignore_type: tp.MaybeTuple[tp.DTypeLike] = None,
+                  ignore_missing: bool = False,
                   na_sentinel: tp.Any = None) -> tp.Any:
     """Apply mapping on object using a mapping-like object.
 
@@ -55,11 +56,12 @@ def apply_mapping(obj: tp.Any,
         reverse (bool): See `reverse` in `to_mapping`.
         ignore_case (bool): Whether to ignore the case if the key is a string.
         ignore_underscores (bool): Whether to ignore underscores if the key is a string.
-        ignore_other_types (bool): Whether to ignore other data types. Otherwise, throws an error.
+        ignore_type (dtype_like or tuple): One or multiple types or data types to ignore.
+        ignore_missing (bool): Whether to ignore missing values.
         na_sentinel (any): Value to mark “not found”.
     """
     if mapping_like is None:
-        return na_sentinel
+        return obj
 
     if ignore_case and ignore_underscores:
         key_func = lambda x: x.lower().replace('_', '')
@@ -69,10 +71,11 @@ def apply_mapping(obj: tp.Any,
         key_func = lambda x: x.replace('_', '')
     else:
         key_func = lambda x: x
+    if not isinstance(ignore_type, tuple):
+        ignore_type = (ignore_type,)
 
     mapping = to_mapping(mapping_like, reverse=reverse)
 
-    key_types = set()
     new_mapping = dict()
     for k, v in mapping.items():
         if pd.isnull(k):
@@ -81,21 +84,27 @@ def apply_mapping(obj: tp.Any,
             if isinstance(k, str):
                 k = key_func(k)
             new_mapping[k] = v
-            key_types.add(type(k))
 
-    def _type_in_key_types(x_type: type) -> bool:
-        for key_type in key_types:
-            if x_type is key_type:
+    def _compatible_types(x_type: type, item: tp.Any = None) -> bool:
+        if item is not None:
+            if np.dtype(x_type) == 'O':
+                x_type = type(item)
+        for y_type in ignore_type:
+            if y_type is None:
+                return False
+            if x_type is y_type:
                 return True
             x_dtype = np.dtype(x_type)
-            key_dtype = np.dtype(key_type)
-            if x_dtype is key_dtype:
+            y_dtype = np.dtype(y_type)
+            if x_dtype is y_dtype:
+                return True
+            if np.issubdtype(x_dtype, np.integer) and np.issubdtype(y_dtype, np.integer):
                 return True
-            if np.issubdtype(x_dtype, np.number) and np.issubdtype(key_dtype, np.number):
+            if np.issubdtype(x_dtype, np.floating) and np.issubdtype(y_dtype, np.floating):
                 return True
-            if np.issubdtype(x_dtype, np.bool_) and np.issubdtype(key_dtype, np.bool_):
+            if np.issubdtype(x_dtype, np.bool_) and np.issubdtype(y_dtype, np.bool_):
                 return True
-            if np.issubdtype(x_dtype, np.flexible) and np.issubdtype(key_dtype, np.flexible):
+            if np.issubdtype(x_dtype, np.flexible) and np.issubdtype(y_dtype, np.flexible):
                 return True
         return False
 
@@ -104,10 +113,13 @@ def _converter(x: tp.Any) -> tp.Any:
             return na_sentinel
         if isinstance(x, str):
             x = key_func(x)
+        if ignore_missing:
+            try:
+                return new_mapping[x]
+            except KeyError:
+                return x
         return new_mapping[x]
 
-    if _type_in_key_types(type(obj)):
-        return _converter(obj)
     if isinstance(obj, (tuple, list, set, frozenset)):
         result = [apply_mapping(
             v,
@@ -115,46 +127,41 @@ def _converter(x: tp.Any) -> tp.Any:
             reverse=reverse,
             ignore_case=ignore_case,
             ignore_underscores=ignore_underscores,
-            ignore_other_types=ignore_other_types,
+            ignore_type=ignore_type,
+            ignore_missing=ignore_missing,
             na_sentinel=na_sentinel
         ) for v in obj]
         return type(obj)(result)
     if isinstance(obj, np.ndarray):
         if obj.size == 0:
             return obj
-        if _type_in_key_types(type(obj.item(0))):
+        if ignore_type is None or not _compatible_types(obj.dtype, obj.item(0)):
+            if obj.ndim == 1:
+                return pd.Series(obj).map(_converter).values
             return np.vectorize(_converter)(obj)
-        if not ignore_other_types:
-            raise ValueError(f"Type is {type(obj.item(0))}, must be one of types {key_types}")
         return obj
     if isinstance(obj, pd.Series):
         if obj.size == 0:
             return obj
-        if _type_in_key_types(type(obj.iloc[0])):
+        if ignore_type is None or not _compatible_types(obj.dtype, obj.iloc[0]):
             return obj.map(_converter)
-        if not ignore_other_types:
-            raise ValueError(f"Type is {type(obj.iloc[0])}, must be one of types {key_types}")
         return obj
     if isinstance(obj, pd.Index):
         if obj.size == 0:
             return obj
-        if _type_in_key_types(type(obj[0])):
+        if ignore_type is None or not _compatible_types(obj.dtype, obj[0]):
             return obj.map(_converter)
-        if not ignore_other_types:
-            raise ValueError(f"Type is {type(obj[0])}, must be one of types {key_types}")
         return obj
     if isinstance(obj, pd.DataFrame):
         if obj.size == 0:
             return obj
         series = []
         for sr_name, sr in obj.iteritems():
-            if _type_in_key_types(type(sr.iloc[0])):
+            if ignore_type is None or not _compatible_types(sr.dtype, sr.iloc[0]):
                 series.append(sr.map(_converter))
             else:
-                if not ignore_other_types:
-                    raise ValueError(f"Type is {type(sr.iloc[0])}, must be one of types {key_types}")
                 series.append(sr)
         return pd.concat(series, axis=1, keys=obj.columns)
-    if not ignore_other_types:
-        raise ValueError(f"Type is {type(obj)}, must be one of types {key_types}")
+    if ignore_type is None or not _compatible_types(type(obj)):
+        return _converter(obj)
     return obj
diff --git a/vectorbt/utils/params.py b/vectorbt/utils/params.py
index 11913848..a9bf54b2 100644
--- a/vectorbt/utils/params.py
+++ b/vectorbt/utils/params.py
@@ -52,8 +52,8 @@ def create_param_combs(op_tree: tp.Tuple, depth: int = 0) -> tp.List[tp.List]:
      [4, 5, 4, 5, 4, 5, 4, 5, 4, 5, 4, 5]]
     ```
     """
-    checks.assert_type(op_tree, tuple)
-    checks.assert_type(op_tree[0], Callable)
+    checks.assert_instance_of(op_tree, tuple)
+    checks.assert_instance_of(op_tree[0], Callable)
     new_op_tree: tp.Tuple = (op_tree[0],)
     for elem in op_tree[1:]:
         if isinstance(elem, tuple) and isinstance(elem[0], Callable):
diff --git a/vectorbt/utils/schedule.py b/vectorbt/utils/schedule.py
index b3b8b0cc..d8850edd 100644
--- a/vectorbt/utils/schedule.py
+++ b/vectorbt/utils/schedule.py
@@ -86,7 +86,7 @@ class ScheduleManager:
     def __init__(self, scheduler: tp.Optional[AsyncScheduler] = None) -> None:
         if scheduler is None:
             scheduler = AsyncScheduler()
-        checks.assert_type(scheduler, AsyncScheduler)
+        checks.assert_instance_of(scheduler, AsyncScheduler)
 
         self._scheduler = scheduler
         self._async_task = None
diff --git a/vectorbt/utils/tags.py b/vectorbt/utils/tags.py
index 38880af1..8f748223 100644
--- a/vectorbt/utils/tags.py
+++ b/vectorbt/utils/tags.py
@@ -42,7 +42,7 @@ def match_tags(tags: tp.MaybeIterable[str], in_tags: tp.MaybeIterable[str]) -> b
     for t in tags:
         if not t.isidentifier():
             node_ids = [node.id for node in ast.walk(ast.parse(t)) if type(node) is ast.Name]
-            eval_mapping = {_id: _id in in_tags for _id in node_ids}
+            eval_mapping = {id_: id_ in in_tags for id_ in node_ids}
             eval_result = RepEval(t).eval(eval_mapping)
             if not isinstance(eval_result, bool):
                 raise TypeError(f"Tag expression '{t}' must produce a boolean")


Test patch:
diff --git a/tests/notebooks/portfolio.ipynb b/tests/notebooks/portfolio.ipynb
index 8b60279a..63f601c7 100644
--- a/tests/notebooks/portfolio.ipynb
+++ b/tests/notebooks/portfolio.ipynb
@@ -6555,7 +6555,7 @@
       "Total Trades                                  2.0\n",
       "Total Closed Trades                      1.333333\n",
       "Total Open Trades                        0.666667\n",
-      "Open Trade P&L                          -1.504206\n",
+      "Open Trade PnL                          -1.504206\n",
       "Win Rate [%]                            33.333333\n",
       "Best Trade [%]                         -98.380588\n",
       "Worst Trade [%]                       -100.803855\n",
@@ -6610,7 +6610,7 @@
       "Total Trades                                    2\n",
       "Total Closed Trades                             1\n",
       "Total Open Trades                               1\n",
-      "Open Trade P&L                            -0.2005\n",
+      "Open Trade PnL                            -0.2005\n",
       "Win Rate [%]                                  0.0\n",
       "Best Trade [%]                         -54.450495\n",
       "Worst Trade [%]                        -54.450495\n",
@@ -6655,7 +6655,7 @@
       "Total Trades                                    2\n",
       "Total Closed Trades                             1\n",
       "Total Open Trades                               1\n",
-      "Open Trade P&L                            -0.2005\n",
+      "Open Trade PnL                            -0.2005\n",
       "Win Rate [%]                                  0.0\n",
       "Best Trade [%]                         -54.450495\n",
       "Worst Trade [%]                        -54.450495\n",
@@ -6700,7 +6700,7 @@
       "Total Trades                                    2\n",
       "Total Closed Trades                             1\n",
       "Total Open Trades                               1\n",
-      "Open Trade P&L                            -0.2005\n",
+      "Open Trade PnL                            -0.2005\n",
       "Win Rate [%]                                  0.0\n",
       "Best Trade [%]                          -3.970297\n",
       "Worst Trade [%]                        -54.450495\n",
@@ -6744,7 +6744,7 @@
        "Total Trades                                    4\n",
        "Total Closed Trades                             2\n",
        "Total Open Trades                               2\n",
-       "Open Trade P&L                          -4.512618\n",
+       "Open Trade PnL                          -4.512618\n",
        "Win Rate [%]                                  0.0\n",
        "Best Trade [%]                         -54.450495\n",
        "Worst Trade [%]                       -388.242424\n",
diff --git a/tests/notebooks/records.ipynb b/tests/notebooks/records.ipynb
index c1edc2a8..6c75ab69 100644
--- a/tests/notebooks/records.ipynb
+++ b/tests/notebooks/records.ipynb
@@ -5553,7 +5553,7 @@
       "Total Short Trades                              0\n",
       "Total Closed Trades                             3\n",
       "Total Open Trades                               1\n",
-      "Open Trade P&L                              -0.16\n",
+      "Open Trade PnL                              -0.16\n",
       "Win Rate [%]                                100.0\n",
       "Max Win Streak                                  3\n",
       "Max Loss Streak                                 0\n",
@@ -5578,7 +5578,7 @@
       "Total Short Trades                              0\n",
       "Total Closed Trades                             3\n",
       "Total Open Trades                               1\n",
-      "Open Trade P&L                              -0.16\n",
+      "Open Trade PnL                              -0.16\n",
       "Win Rate [%]                                100.0\n",
       "Max Win Streak                                  3\n",
       "Max Loss Streak                                 0\n",
@@ -5621,7 +5621,7 @@
       "Total Short Trades                           1.25\n",
       "Total Closed Trades                           2.5\n",
       "Total Open Trades                            0.75\n",
-      "Open Trade P&L                               -0.1\n",
+      "Open Trade PnL                               -0.1\n",
       "Win Rate [%]                            58.333333\n",
       "Max Win Streak                                2.0\n",
       "Max Loss Streak                          1.333333\n",
@@ -5890,7 +5890,7 @@
       "Total Short Positions                              0\n",
       "Total Closed Positions                             2\n",
       "Total Open Positions                               1\n",
-      "Open Position P&L                              -0.16\n",
+      "Open Position PnL                              -0.16\n",
       "Win Rate [%]                                   100.0\n",
       "Max Win Streak                                     2\n",
       "Max Loss Streak                                    0\n",
@@ -5916,7 +5916,7 @@
       "Total Short Positions                              0\n",
       "Total Closed Positions                             2\n",
       "Total Open Positions                               1\n",
-      "Open Position P&L                              -0.16\n",
+      "Open Position PnL                              -0.16\n",
       "Win Rate [%]                                   100.0\n",
       "Max Win Streak                                     2\n",
       "Max Loss Streak                                    0\n",
@@ -5958,7 +5958,7 @@
       "Total Short Positions                            1.0\n",
       "Total Closed Positions                          1.75\n",
       "Total Open Positions                            0.75\n",
-      "Open Position P&L                               -0.1\n",
+      "Open Position PnL                               -0.1\n",
       "Win Rate [%]                               55.555556\n",
       "Max Win Streak                              1.333333\n",
       "Max Loss Streak                                  1.0\n",
diff --git a/tests/test_base.py b/tests/test_base.py
index dc6733f3..d592fc85 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -168,9 +168,9 @@ def test_get_group_lens_nb(self):
             column_grouper.get_group_lens_nb(np.array([])),
             np.array([])
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             column_grouper.get_group_lens_nb(np.array([1, 1, 0, 0]))
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             column_grouper.get_group_lens_nb(np.array([0, 1, 0, 1]))
 
     def test_is_grouped(self):
@@ -247,16 +247,16 @@ def test_is_group_count_changed(self):
 
     def test_check_group_by(self):
         column_grouper.ColumnGrouper(grouped_columns, group_by=None, allow_enable=True).check_group_by(group_by=0)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             column_grouper.ColumnGrouper(grouped_columns, group_by=None, allow_enable=False).check_group_by(group_by=0)
         column_grouper.ColumnGrouper(grouped_columns, group_by=0, allow_disable=True).check_group_by(group_by=False)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             column_grouper.ColumnGrouper(grouped_columns, group_by=0, allow_disable=False).check_group_by(
                 group_by=False)
         column_grouper.ColumnGrouper(grouped_columns, group_by=0, allow_modify=True).check_group_by(group_by=1)
         column_grouper.ColumnGrouper(grouped_columns, group_by=0, allow_modify=False).check_group_by(
             group_by=np.array([2, 2, 2, 2, 3, 3, 3, 3]))
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             column_grouper.ColumnGrouper(grouped_columns, group_by=0, allow_modify=False).check_group_by(group_by=1)
 
     def test_resolve_group_by(self):
@@ -388,9 +388,9 @@ def test_indexing_func_meta(self):
         np.testing.assert_array_equal(a, np.array([0, 1]))
         np.testing.assert_array_equal(b, np.array([0, 1]))
         np.testing.assert_array_equal(c, np.array([0, 1]))
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = df4_wrapper.indexing_func_meta(lambda x: x.iloc[0, 0])[1:]
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = df4_wrapper.indexing_func_meta(lambda x: x.iloc[[0], 0])[1:]
 
         # not grouped, column only
@@ -406,9 +406,9 @@ def test_indexing_func_meta(self):
         np.testing.assert_array_equal(a, np.array([0, 1, 2]))
         np.testing.assert_array_equal(b, np.array([0, 1]))
         np.testing.assert_array_equal(c, np.array([0, 1]))
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = sr2_wrapper_co.indexing_func_meta(lambda x: x.iloc[:2])[1:]
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = df4_wrapper_co.indexing_func_meta(lambda x: x.iloc[:, :2])[1:]
 
         # grouped
@@ -432,7 +432,7 @@ def test_indexing_func_meta(self):
         np.testing.assert_array_equal(a, np.array([0, 1]))
         np.testing.assert_array_equal(b, np.array([0, 1]))
         np.testing.assert_array_equal(c, np.array([0, 1, 2]))
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = df4_grouped_wrapper.indexing_func_meta(lambda x: x.iloc[0, :2])[1:]
 
         # grouped, column only
@@ -739,6 +739,39 @@ def test_wrap(self):
             array_wrapper.ArrayWrapper.from_obj(df2).wrap(a2, index=df4.index),
             pd.DataFrame(a2, index=df4.index, columns=df2.columns)
         )
+        pd.testing.assert_frame_equal(
+            array_wrapper.ArrayWrapper(index=df4.index, columns=df4.columns, ndim=2).wrap(
+                np.array([[0, 0, np.nan], [1, np.nan, 1], [2, 2, np.nan]]),
+                fillna=-1
+            ),
+            pd.DataFrame([
+                [0., 0., -1.],
+                [1., -1., 1.],
+                [2., 2., -1.]
+            ], index=df4.index, columns=df4.columns)
+        )
+        pd.testing.assert_frame_equal(
+            array_wrapper.ArrayWrapper(index=df4.index, columns=df4.columns, ndim=2).wrap(
+                np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2]]),
+                to_index=True
+            ),
+            pd.DataFrame([
+                ['x6', 'x6', 'x6'],
+                ['y6', 'y6', 'y6'],
+                ['z6', 'z6', 'z6']
+            ], index=df4.index, columns=df4.columns)
+        )
+        pd.testing.assert_frame_equal(
+            array_wrapper.ArrayWrapper(index=df4.index, columns=df4.columns, ndim=2, freq='d').wrap(
+                np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2]]),
+                to_timedelta=True
+            ),
+            pd.DataFrame([
+                [pd.Timedelta(days=0), pd.Timedelta(days=0), pd.Timedelta(days=0)],
+                [pd.Timedelta(days=1), pd.Timedelta(days=1), pd.Timedelta(days=1)],
+                [pd.Timedelta(days=2), pd.Timedelta(days=2), pd.Timedelta(days=2)]
+            ], index=df4.index, columns=df4.columns)
+        )
 
     def test_wrap_reduced(self):
         # sr to value
@@ -951,9 +984,9 @@ def test_select_one(self):
             df4_grouped_wrapping.select_one(column='g1').wrapper.get_columns(),
             pd.Index(['g1'], dtype='object')
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df4_wrapping.select_one()
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df4_grouped_wrapping.select_one()
 
 
@@ -1149,7 +1182,7 @@ def test_drop_levels(self):
             index_fns.drop_levels(multi_i, 'i9', strict=False),
             multi_i
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = index_fns.drop_levels(multi_i, 'i9')
         pd.testing.assert_index_equal(
             index_fns.drop_levels(multi_i, ['i7', 'i8'], strict=False),  # won't do anything
@@ -1159,7 +1192,7 @@ def test_drop_levels(self):
                 ('z7', 'z8')
             ], names=['i7', 'i8'])
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = index_fns.drop_levels(multi_i, ['i7', 'i8'])
 
     def test_rename_levels(self):
@@ -1172,7 +1205,7 @@ def test_rename_levels(self):
             index_fns.rename_levels(i, {'a': 'b'}, strict=False),
             i
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = index_fns.rename_levels(i, {'a': 'b'}, strict=True)
         pd.testing.assert_index_equal(
             index_fns.rename_levels(multi_i, {'i7': 'f7', 'i8': 'f8'}),
@@ -1278,7 +1311,7 @@ def test_align_index_to(self):
             index_fns.align_index_to(index1, index2),
             np.array([2, 1, 0, 2, 1, 0])
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             index_fns.align_index_to(pd.Index(['a']), pd.Index(['a', 'b', 'c']))
         index3 = pd.MultiIndex.from_tuples([
             (0, 'c'),
@@ -1292,12 +1325,12 @@ def test_align_index_to(self):
             index_fns.align_index_to(index1, index3),
             np.array([0, 1, 2, 0, 1, 2])
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             index_fns.align_index_to(
                 pd.Index(['b', 'a'], name='name1'),
                 index3
             )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             index_fns.align_index_to(
                 pd.Index(['c', 'b', 'a', 'a'], name='name1'),
                 index3
@@ -1370,9 +1403,9 @@ def test_pick_levels(self):
                == ([3, 2, 1], [0])
         assert index_fns.pick_levels(index, required_levels=[None, None, None, None], optional_levels=[None]) \
                == ([0, 1, 2, 3], [None])
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             index_fns.pick_levels(index, required_levels=['i8', 'i8', 'i8', 'i8'], optional_levels=[])
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             index_fns.pick_levels(index, required_levels=['c8', 'c7', 'i8', 'i7'], optional_levels=['i7'])
 
 
@@ -1731,7 +1764,7 @@ def test_broadcast_idx(self):
                     name=sr2.name
                 )
             )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = reshape_fns.broadcast(
                 *to_broadcast,
                 index_from=0,
@@ -1770,7 +1803,7 @@ def test_broadcast_idx(self):
     def test_broadcast_strict(self):
         # 1d
         to_broadcast = sr1, sr2
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = reshape_fns.broadcast(
                 *to_broadcast,
                 index_from='strict',  # changing index not allowed
@@ -1781,7 +1814,7 @@ def test_broadcast_strict(self):
             )
         # 2d
         to_broadcast = df1, df2
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = reshape_fns.broadcast(
                 *to_broadcast,
                 index_from='stack',
@@ -2977,7 +3010,7 @@ def apply_func_nb(i, x, y, c, d):
             target
         )
         if ray_available:
-            with pytest.raises(Exception) as e_info:
+            with pytest.raises(Exception):
                 sr2.vbt.apply_and_concat(
                     3, np.array([1, 2, 3]), 10, 100, apply_func=apply_func_nb, numba_loop=True, use_ray=True,
                     keys=['a', 'b', 'c']
@@ -3142,7 +3175,7 @@ def combine_func3_nb(x, y):
             target
         )
         if ray_available:
-            with pytest.raises(Exception) as e_info:
+            with pytest.raises(Exception):
                 sr2.vbt.combine(
                     [10, df4], 10, 100,
                     combine_func=combine_func_nb, numba_loop=True, use_ray=True
diff --git a/tests/test_data.py b/tests/test_data.py
index 2d8bdcf1..8f90af9d 100644
--- a/tests/test_data.py
+++ b/tests/test_data.py
@@ -342,16 +342,16 @@ def test_download(self):
                 columns=pd.Int64Index([1], dtype='int64')
             )
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             MyData.download([0, 1], shape=(5, 3), index_mask=index_mask, column_mask=column_mask,
                             missing_index='raise', missing_columns='nan')
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             MyData.download([0, 1], shape=(5, 3), index_mask=index_mask, column_mask=column_mask,
                             missing_index='nan', missing_columns='raise')
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             MyData.download([0, 1], shape=(5, 3), index_mask=index_mask, column_mask=column_mask,
                             missing_index='test', missing_columns='nan')
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             MyData.download([0, 1], shape=(5, 3), index_mask=index_mask, column_mask=column_mask,
                             missing_index='nan', missing_columns='test')
 
diff --git a/tests/test_generic.py b/tests/test_generic.py
index e773670b..5fd27f6f 100644
--- a/tests/test_generic.py
+++ b/tests/test_generic.py
@@ -74,6 +74,72 @@ def teardown_module():
 
 
 class TestAccessors:
+    def test_set_by_mask(self):
+        np.testing.assert_array_equal(
+            nb.set_by_mask_1d_nb(
+                np.array([1, 2, 3, 1, 2, 3]),
+                np.array([True, False, False, True, False, False]),
+                0
+            ),
+            np.array([0, 2, 3, 0, 2, 3])
+        )
+        np.testing.assert_array_equal(
+            nb.set_by_mask_1d_nb(
+                np.array([1, 2, 3, 1, 2, 3]),
+                np.array([True, False, False, True, False, False]),
+                0.
+            ),
+            np.array([0., 2., 3., 0., 2., 3.])
+        )
+        np.testing.assert_array_equal(
+            nb.set_by_mask_nb(
+                np.array([1, 2, 3, 1, 2, 3])[:, None],
+                np.array([True, False, False, True, False, False])[:, None],
+                0
+            ),
+            np.array([0, 2, 3, 0, 2, 3])[:, None]
+        )
+        np.testing.assert_array_equal(
+            nb.set_by_mask_nb(
+                np.array([1, 2, 3, 1, 2, 3])[:, None],
+                np.array([True, False, False, True, False, False])[:, None],
+                0.
+            ),
+            np.array([0., 2., 3., 0., 2., 3.])[:, None]
+        )
+        np.testing.assert_array_equal(
+            nb.set_by_mask_mult_1d_nb(
+                np.array([1, 2, 3, 1, 2, 3]),
+                np.array([True, False, False, True, False, False]),
+                np.array([0, -1, -1, 0, -1, -1])
+            ),
+            np.array([0, 2, 3, 0, 2, 3])
+        )
+        np.testing.assert_array_equal(
+            nb.set_by_mask_mult_1d_nb(
+                np.array([1, 2, 3, 1, 2, 3]),
+                np.array([True, False, False, True, False, False]),
+                np.array([0., -1., -1., 0., -1., -1.])
+            ),
+            np.array([0., 2., 3., 0., 2., 3.])
+        )
+        np.testing.assert_array_equal(
+            nb.set_by_mask_mult_nb(
+                np.array([1, 2, 3, 1, 2, 3])[:, None],
+                np.array([True, False, False, True, False, False])[:, None],
+                np.array([0, -1, -1, 0, -1, -1])[:, None]
+            ),
+            np.array([0, 2, 3, 0, 2, 3])[:, None]
+        )
+        np.testing.assert_array_equal(
+            nb.set_by_mask_mult_nb(
+                np.array([1, 2, 3, 1, 2, 3])[:, None],
+                np.array([True, False, False, True, False, False])[:, None],
+                np.array([0., -1., -1., 0., -1., -1.])[:, None]
+            ),
+            np.array([0., 2., 3., 0., 2., 3.])[:, None]
+        )
+
     def test_shuffle(self):
         pd.testing.assert_series_equal(
             df['a'].vbt.shuffle(seed=seed),
@@ -664,6 +730,7 @@ def test_squeeze_grouped(self):
                 [1.0, 1.0]
             ], index=df.index, columns=['g1', 'g2'])
         )
+        assert df['a'].vbt.squeeze_grouped(i_col_nanmean_nb, group_by=True) == 2.5
 
     def test_flatten_grouped(self):
         pd.testing.assert_frame_equal(
@@ -696,6 +763,18 @@ def test_flatten_grouped(self):
                 [1.0, np.nan]
             ], index=np.tile(df.index, 2), columns=['g1', 'g2'])
         )
+        pd.testing.assert_series_equal(
+            pd.DataFrame([[False, True], [False, True]]).vbt.flatten_grouped(group_by=True, order='C'),
+            pd.Series([False, True, False, True], name='group')
+        )
+        pd.testing.assert_series_equal(
+            pd.DataFrame([[False, True], [False, True]]).vbt.flatten_grouped(group_by=True, order='F'),
+            pd.Series([False, False, True, True], name='group')
+        )
+        pd.testing.assert_frame_equal(
+            pd.Series([False, True, True, False]).vbt.flatten_grouped(group_by=[0, 0, 0, 1]),
+            pd.DataFrame([[0., 0.], [1., np.nan], [1., np.nan]], columns=pd.Int64Index([0, 1], dtype='int64'))
+        )
 
     @pytest.mark.parametrize(
         "test_name,test_func,test_func_nb",
@@ -1249,15 +1328,15 @@ def test_range_split(self):
                 df.vbt.range_split(start_idxs=df.index[[0]], end_idxs=df.index[[2, 3]])[1][i],
                 target[i]
             )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.range_split()
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.range_split(start_idxs=[0, 1])
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.range_split(end_idxs=[2, 4])
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.range_split(min_len=10)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.range_split(n=10)
 
     def test_rolling_split(self):
@@ -1479,15 +1558,15 @@ def test_rolling_split(self):
                 indexes1[i],
                 target[i]
             )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.rolling_split()
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.rolling_split(window_len=3, set_lens=(3, 1))
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.rolling_split(window_len=1, set_lens=(1, 1))
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.rolling_split(n=2, min_len=10)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.rolling_split(n=10)
 
     def test_expanding_split(self):
@@ -1580,9 +1659,9 @@ def test_expanding_split(self):
                 indexes1[i],
                 target[i]
             )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.expanding_split(n=2, min_len=10)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             df.vbt.expanding_split(n=10)
 
     def test_stats(self):
diff --git a/tests/test_indicators.py b/tests/test_indicators.py
index 6163bc9a..0a3bace6 100644
--- a/tests/test_indicators.py
+++ b/tests/test_indicators.py
@@ -1,4 +1,3 @@
-import vectorbt as vbt
 import numpy as np
 import pandas as pd
 from numba import njit
@@ -372,7 +371,7 @@ def apply_func_nb(p):
             F.from_apply_func(apply_func_nb, numba_loop=True).run([0, 1]).out,
             target
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             F.from_apply_func(apply_func).run([0, 1], per_column=True)
 
     def test_input_shape(self):
@@ -575,7 +574,7 @@ def apply_func_nb():
             F.from_apply_func(apply_func_nb, numba_loop=True).run().out,
             pd.DataFrame(np.full((3, 3), 1))
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             F.from_apply_func(apply_func).run(per_column=True)
 
     def test_multiple_params(self):
@@ -645,9 +644,9 @@ def apply_func_nb(ts, p1, p2):
             F.from_apply_func(apply_func).run(ts, np.asarray([0, 1, 2]), np.array([2]), per_column=True).out,
             target
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             F.from_apply_func(apply_func).run(ts, np.asarray([0, 1]), 2, per_column=True)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             F.from_apply_func(apply_func).run(ts, np.asarray([0, 1, 2, 3]), 2, per_column=True)
 
     def test_param_settings(self):
@@ -869,7 +868,7 @@ def apply_func(ts1, ts2, in_out1, in_out2, p1, p2):
             F.from_apply_func(apply_func, hide_default=False)
                 .run(ts, ts, [1, 2], [1, 2]).out
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             pd.testing.assert_frame_equal(
                 F.from_apply_func(apply_func, in_out1=1, in_out2=2)
                     .run(ts, ts, [1, 2], 3).in_out1,
@@ -2070,6 +2069,30 @@ def test_mapping_attr(self):
             )
         )
 
+    def test_stats(self):
+        @njit
+        def apply_func_nb(ts):
+            return ts ** 2, ts ** 3
+
+        MyInd = vbt.IndicatorFactory(
+            input_names=['ts'],
+            output_names=['out1', 'out2'],
+            metrics=dict(
+                sum_diff=dict(
+                    calc_func=lambda self, const: self.out2.sum() * self.out1.sum() + const
+                )
+            ),
+            stats_defaults=dict(settings=dict(const=1000))
+        ).from_apply_func(
+            apply_func_nb
+        )
+
+        myind = MyInd.run(ts)
+        pd.testing.assert_series_equal(
+            myind.stats(),
+            pd.Series([9535.0], index=['sum_diff'], name='agg_func_mean')
+        )
+
     def test_dir(self):
         TestEnum = namedtuple('TestEnum', ['Hello', 'World'])(0, 1)
         F = vbt.IndicatorFactory(
@@ -2314,22 +2337,6 @@ def test_from_pandas_ta(self):
                     columns=pd.Int64Index([2, 3, 4], dtype='int64', name='sma_length')
                 )
             )
-            pd.testing.assert_series_equal(
-                vbt.pandas_ta('STOCH').run(ts['a'], ts['b'], ts['c'], k=2, d=2).stochk,
-                pd.Series([np.nan, np.nan, np.nan, 33.333333333333336, 0.0], index=ts.index, name=(2, 2))
-            )
-            pd.testing.assert_series_equal(
-                vbt.pandas_ta('STOCH').run(ts['a'], ts['b'], ts['c'], k=2, d=2).stochd,
-                pd.Series([np.nan, np.nan, np.nan, np.nan, 16.666666666666668], index=ts.index, name=(2, 2))
-            )
-            pd.testing.assert_series_equal(
-                vbt.pandas_ta('PVR').run(ts['a'], ts['b']).pvr,
-                pd.Series([1.0, 2.0, 2.0, 2.0, 2.0], index=ts.index)
-            )
-            pd.testing.assert_series_equal(
-                vbt.pandas_ta('ICHIMOKU').run(ts['a'], ts['b'], ts['c'], 2, 2, 2).isa,
-                pd.Series([np.nan, np.nan, np.nan, 3.0, 3.0], index=ts.index, name=(2, 2, 2))
-            )
 
     def test_get_ta_indicators(self):
         if ta_available:
diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py
index 7fef3377..e8901fbb 100644
--- a/tests/test_portfolio.py
+++ b/tests/test_portfolio.py
@@ -53,115 +53,115 @@ def assert_same_tuple(tup1, tup2):
 
 def test_execute_order_nb():
     # Errors, ignored and rejected orders
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(-100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(np.nan, 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., np.inf, 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., np.nan, 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., np.nan, 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., -10., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., np.nan, 10., 1100., 0, 0),
             nb.order_nb(10, 10))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, size_type=-2))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, size_type=20))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, direction=-2))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, direction=20))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., -100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, direction=Direction.LongOnly))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, direction=Direction.ShortOnly))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, np.inf))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, -10))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, fees=np.inf))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, fees=-1))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, fixed_fees=np.inf))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, fixed_fees=-1))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, slippage=np.inf))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, slippage=-1))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, min_size=np.inf))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, min_size=-1))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, max_size=0))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, max_size=-10))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, reject_prob=np.nan))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, reject_prob=-1))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., 10., 1100., 0, 0),
             nb.order_nb(10, 10, reject_prob=2))
@@ -178,11 +178,11 @@ def test_execute_order_nb():
     assert exec_state == ExecuteOrderState(cash=100.0, position=100.0, debt=0.0, free_cash=100.0)
     assert_same_tuple(order_result, OrderResult(
         size=np.nan, price=np.nan, fees=np.nan, side=-1, status=2, status_info=4))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., np.inf, 1100., 0, 0),
             nb.order_nb(10, 10, size_type=SizeType.Value))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., -10., 1100, 0, 0),
             nb.order_nb(10, 10, size_type=SizeType.Value))
@@ -190,11 +190,11 @@ def test_execute_order_nb():
         ProcessOrderState(100., 100., 0., 100., np.nan, 1100., 0, 0),
         nb.order_nb(10, 10, size_type=SizeType.Value))
     assert exec_state == ExecuteOrderState(cash=100.0, position=100.0, debt=0.0, free_cash=100.0)
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., np.inf, 1100., 0, 0),
             nb.order_nb(10, 10, size_type=SizeType.TargetValue))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(100., 100., 0., 100., -10., 1100, 0, 0),
             nb.order_nb(10, 10, size_type=SizeType.TargetValue))
@@ -246,11 +246,11 @@ def test_execute_order_nb():
     assert exec_state == ExecuteOrderState(cash=0.0, position=100.0, debt=0.0, free_cash=0.0)
     assert_same_tuple(order_result, OrderResult(
         size=np.nan, price=np.nan, fees=np.nan, side=-1, status=2, status_info=7))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(np.inf, 100, 0., np.inf, np.nan, 1100., 0, 0),
             nb.order_nb(np.inf, 10, direction=Direction.LongOnly))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(np.inf, 100., 0., np.inf, 10., 1100., 0, 0),
             nb.order_nb(np.inf, 10, direction=Direction.All))
@@ -260,11 +260,11 @@ def test_execute_order_nb():
     assert exec_state == ExecuteOrderState(cash=100.0, position=0.0, debt=0.0, free_cash=100.0)
     assert_same_tuple(order_result, OrderResult(
         size=np.nan, price=np.nan, fees=np.nan, side=-1, status=2, status_info=8))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(np.inf, 100., 0., np.inf, 10., 1100., 0, 0),
             nb.order_nb(-np.inf, 10, direction=Direction.ShortOnly))
-    with pytest.raises(Exception) as e_info:
+    with pytest.raises(Exception):
         _ = nb.execute_order_nb(
             ProcessOrderState(np.inf, 100., 0., np.inf, 10., 1100., 0, 0),
             nb.order_nb(-np.inf, 10, direction=Direction.All))
@@ -689,7 +689,7 @@ def test_value(self):
         )
 
     def test_percent(self):
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_all(size=0.5, size_type='percent')
         record_arrays_close(
             from_signals_all(size=0.5, size_type='percent', close_first=True).order_records,
@@ -1073,13 +1073,13 @@ def test_raise_reject(self):
                 (0, 0, 0, 100.0, 1.0, 0.0, 0), (1, 3, 0, 100.0, 4.0, 0.0, 1)
             ], dtype=order_dt)
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_shortonly(size=1000, allow_partial=True, raise_reject=True).order_records
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_all(size=1000, allow_partial=False, raise_reject=True).order_records
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_longonly(size=1000, allow_partial=False, raise_reject=True).order_records
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_shortonly(size=1000, allow_partial=False, raise_reject=True).order_records
 
     def test_log(self):
@@ -1185,11 +1185,11 @@ def test_init_cash(self):
                 (3, 3, 1, 0.5, 4.0, 0.0, 0), (4, 0, 2, 1.0, 1.0, 0.0, 1), (5, 3, 2, 1.0, 4.0, 0.0, 0)
             ], dtype=order_dt)
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_all(init_cash=np.inf).order_records
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_longonly(init_cash=np.inf).order_records
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_shortonly(init_cash=np.inf).order_records
 
     def test_group_by(self):
@@ -1229,7 +1229,7 @@ def test_cash_sharing(self):
             pd.Series([100., 100.], index=pd.Int64Index([0, 1], dtype='int64')).rename('init_cash')
         )
         assert pf.cash_sharing
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = pf.regroup(group_by=False)
 
     def test_call_seq(self):
@@ -1396,7 +1396,7 @@ def test_sl_stop(self):
         entries = pd.Series([True, False, False, False, False], index=price.index)
         exits = pd.Series([False, False, False, False, False], index=price.index)
 
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_all(sl_stop=-0.1)
 
         close = pd.Series([5., 4., 3., 2., 1.], index=price.index)
@@ -1541,7 +1541,7 @@ def test_ts_stop(self):
         entries = pd.Series([True, False, False, False, False], index=price.index)
         exits = pd.Series([False, False, False, False, False], index=price.index)
 
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_all(ts_stop=-0.1)
 
         close = pd.Series([4., 5., 4., 3., 2.], index=price.index)
@@ -1686,7 +1686,7 @@ def test_tp_stop(self):
         entries = pd.Series([True, False, False, False, False], index=price.index)
         exits = pd.Series([False, False, False, False, False], index=price.index)
 
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_all(sl_stop=-0.1)
 
         close = pd.Series([5., 4., 3., 2., 1.], index=price.index)
@@ -2071,13 +2071,13 @@ def adjust_tp_func_nb(c, dur):
     def test_max_orders(self):
         _ = from_signals_all(close=price_wide)
         _ = from_signals_all(close=price_wide, max_orders=6)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_all(close=price_wide, max_orders=5)
 
     def test_max_logs(self):
         _ = from_signals_all(close=price_wide, log=True)
         _ = from_signals_all(close=price_wide, log=True, max_logs=6)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_signals_all(close=price_wide, log=True, max_logs=5)
 
 
@@ -2819,11 +2819,11 @@ def test_raise_reject(self):
                 (3, 4, 0, 800.0, 5.0, 0.0, 0)
             ], dtype=order_dt)
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_orders_all(size=order_size_one * 1000, allow_partial=False, raise_reject=True).order_records
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_orders_longonly(size=order_size_one * 1000, allow_partial=False, raise_reject=True).order_records
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_orders_shortonly(size=order_size_one * 1000, allow_partial=False, raise_reject=True).order_records
 
     def test_log(self):
@@ -2887,7 +2887,7 @@ def test_cash_sharing(self):
             pd.Series([100., 100.], index=pd.Int64Index([0, 1], dtype='int64')).rename('init_cash')
         )
         assert pf.cash_sharing
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = pf.regroup(group_by=False)
 
     def test_call_seq(self):
@@ -3266,13 +3266,13 @@ def test_auto_seq(self):
     def test_max_orders(self):
         _ = from_orders_all(close=price_wide)
         _ = from_orders_all(close=price_wide, max_orders=9)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_orders_all(close=price_wide, max_orders=8)
 
     def test_max_logs(self):
         _ = from_orders_all(close=price_wide, log=True)
         _ = from_orders_all(close=price_wide, log=True, max_logs=15)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = from_orders_all(close=price_wide, log=True, max_logs=14)
 
 
@@ -3614,7 +3614,7 @@ def test_call_seq(self, test_row_wise):
                 [1, 0, 0]
             ])
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = vbt.Portfolio.from_order_func(
                 price_wide, order_func_nb, np.inf, group_by=np.array([0, 0, 1]),
                 cash_sharing=True, call_seq='auto', row_wise=test_row_wise
@@ -4862,7 +4862,7 @@ def test_max_orders(self, test_row_wise):
             price_wide, order_func_nb, np.inf, row_wise=test_row_wise)
         _ = vbt.Portfolio.from_order_func(
             price_wide, order_func_nb, np.inf, row_wise=test_row_wise, max_orders=15)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = vbt.Portfolio.from_order_func(
                 price_wide, order_func_nb, np.inf, row_wise=test_row_wise, max_orders=14)
 
@@ -4875,7 +4875,7 @@ def test_max_logs(self, test_row_wise):
             price_wide, log_order_func_nb, np.inf, row_wise=test_row_wise)
         _ = vbt.Portfolio.from_order_func(
             price_wide, log_order_func_nb, np.inf, row_wise=test_row_wise, max_logs=15)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = vbt.Portfolio.from_order_func(
                 price_wide, log_order_func_nb, np.inf, row_wise=test_row_wise, max_logs=14)
 
@@ -5059,7 +5059,7 @@ def test_regroup(self):
         assert pf_grouped.regroup(False).wrapper.grouper.group_by is None
         assert pf_grouped.regroup(group_by) == pf_grouped
         assert pf_shared.regroup(None) == pf_shared
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = pf_shared.regroup(False)
         assert pf_shared.regroup(group_by) == pf_shared
 
@@ -5347,7 +5347,9 @@ def test_drawdowns(self):
         record_arrays_close(
             pf.drawdowns.values,
             np.array([
-                (0, 0, 0, 4, 4, 0), (1, 1, 0, 4, 4, 0), (2, 2, 2, 3, 4, 0)
+                (0, 0, 0, 1, 4, 4, 100.0, 99.68951, 99.68951, 0),
+                (1, 1, 0, 1, 4, 4, 99.8801, 95.26858, 95.26858, 0),
+                (2, 2, 2, 3, 3, 4, 101.71618000000001, 101.70822000000001, 101.70822000000001, 0)
             ], dtype=drawdown_dt)
         )
         result = pd.Series(
@@ -6565,7 +6567,7 @@ def test_return_method(self):
                 index=price_na.columns
             ).rename('information_ratio')
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = pf_shared.information_ratio(pf_shared.benchmark_returns(group_by=False) * 2)
 
     def test_stats(self):
@@ -6574,7 +6576,7 @@ def test_stats(self):
             'Total Return [%]', 'Benchmark Return [%]', 'Max Gross Exposure [%]',
             'Total Fees Paid', 'Max Drawdown [%]', 'Max Drawdown Duration',
             'Total Trades', 'Total Closed Trades', 'Total Open Trades',
-            'Open Trade P&L', 'Win Rate [%]', 'Best Trade [%]', 'Worst Trade [%]',
+            'Open Trade PnL', 'Win Rate [%]', 'Best Trade [%]', 'Worst Trade [%]',
             'Avg Winning Trade [%]', 'Avg Losing Trade [%]',
             'Avg Winning Trade Duration', 'Avg Losing Trade Duration',
             'Profit Factor', 'Expectancy', 'Sharpe Ratio', 'Calmar Ratio',
@@ -6639,7 +6641,7 @@ def test_stats(self):
                     'Total Return [%]', 'Benchmark Return [%]', 'Max Gross Exposure [%]',
                     'Total Fees Paid', 'Max Drawdown [%]', 'Max Drawdown Duration',
                     'Total Positions', 'Total Closed Positions', 'Total Open Positions',
-                    'Open Position P&L', 'Win Rate [%]', 'Best Position [%]',
+                    'Open Position PnL', 'Win Rate [%]', 'Best Position [%]',
                     'Worst Position [%]', 'Avg Winning Position [%]',
                     'Avg Losing Position [%]', 'Avg Winning Position Duration',
                     'Avg Losing Position Duration', 'Profit Factor', 'Expectancy',
@@ -6683,7 +6685,7 @@ def test_stats(self):
                     pd.Timedelta('5 days 00:00:00'), 100.0, 99.68951, -0.3104899999999997, 150.0,
                     5.015572852148637, 0.35549, 0.3104900000000015, pd.Timedelta('4 days 00:00:00'),
                     2, 1, 1, -0.20049999999999982, 0.0, -3.9702970297029667, -54.450495049504966,
-                    np.nan, -29.210396039603964, pd.NaT, pd.Timedelta('0 days 12:00:00'), 0.0,
+                    np.nan, -29.210396039603964, pd.NaT, pd.Timedelta('1 days 00:00:00'), 0.0,
                     -0.1552449999999999, -13.30804491478906, -65.40868619923044, 0.0, -11.738864633265454
                 ]),
                 index=stats_index,
@@ -6710,10 +6712,101 @@ def test_stats(self):
                     1, -0.20049999999999982
                 ]),
                 index=pd.Index([
-                    'Total Open Trades', 'Open Trade P&L'
+                    'Total Open Trades', 'Open Trade PnL'
                 ], dtype='object'),
                 name='a')
         )
+        max_winning_streak = (
+            'max_winning_streak',
+            dict(
+                title='Max Winning Streak',
+                calc_func=lambda trades: trades.winning_streak.max(),
+                resolve_trades=True
+            )
+        )
+        pd.testing.assert_series_equal(
+            pf.stats(column='a', metrics=max_winning_streak),
+            pd.Series([0.0], index=['Max Winning Streak'], name='a')
+        )
+        max_winning_streak = (
+            'max_winning_streak',
+            dict(
+                title='Max Winning Streak',
+                calc_func=lambda self, group_by: self.get_trades(group_by=group_by).winning_streak.max()
+            )
+        )
+        pd.testing.assert_series_equal(
+            pf.stats(column='a', metrics=max_winning_streak),
+            pd.Series([0.0], index=['Max Winning Streak'], name='a')
+        )
+        max_winning_streak = (
+            'max_winning_streak',
+            dict(
+                title='Max Winning Streak',
+                calc_func=lambda self, settings:
+                self.get_trades(group_by=settings['group_by']).winning_streak.max(),
+                resolve_calc_func=False
+            )
+        )
+        pd.testing.assert_series_equal(
+            pf.stats(column='a', metrics=max_winning_streak),
+            pd.Series([0.0], index=['Max Winning Streak'], name='a')
+        )
+        vbt.settings.portfolio.stats['settings']['my_arg'] = 100
+        my_arg_metric = ('my_arg_metric', dict(title='My Arg', calc_func=lambda my_arg: my_arg))
+        pd.testing.assert_series_equal(
+            pf.stats(my_arg_metric, column='a'),
+            pd.Series([100], index=['My Arg'], name='a')
+        )
+        vbt.settings.portfolio.stats.reset()
+        pd.testing.assert_series_equal(
+            pf.stats(my_arg_metric, column='a', settings=dict(my_arg=200)),
+            pd.Series([200], index=['My Arg'], name='a')
+        )
+        my_arg_metric = ('my_arg_metric', dict(title='My Arg', my_arg=300, calc_func=lambda my_arg: my_arg))
+        pd.testing.assert_series_equal(
+            pf.stats(my_arg_metric, column='a', settings=dict(my_arg=200)),
+            pd.Series([300], index=['My Arg'], name='a')
+        )
+        pd.testing.assert_series_equal(
+            pf.stats(my_arg_metric, column='a', settings=dict(my_arg=200),
+                     metric_settings=dict(my_arg_metric=dict(my_arg=400))),
+            pd.Series([400], index=['My Arg'], name='a')
+        )
+        trade_min_pnl_cnt = (
+            'trade_min_pnl_cnt',
+            dict(
+                title=vbt.Sub('Trades with PnL over $$${min_pnl}'),
+                calc_func=lambda trades, min_pnl: trades.apply_mask(
+                    trades.pnl.values >= min_pnl).count(),
+                resolve_trades=True
+            )
+        )
+        pd.testing.assert_series_equal(
+            pf.stats(
+                metrics=trade_min_pnl_cnt, column='a',
+                metric_settings=dict(trade_min_pnl_cnt=dict(min_pnl=0))),
+            pd.Series([0], index=['Trades with PnL over $0'], name='a')
+        )
+        pd.testing.assert_series_equal(
+            pf.stats(
+                metrics=[
+                    trade_min_pnl_cnt,
+                    trade_min_pnl_cnt,
+                    trade_min_pnl_cnt
+                ],
+                column='a',
+                metric_settings=dict(
+                    trade_min_pnl_cnt_0=dict(min_pnl=0),
+                    trade_min_pnl_cnt_1=dict(min_pnl=10),
+                    trade_min_pnl_cnt_2=dict(min_pnl=20))
+            ),
+            pd.Series([0, 0, 0], index=[
+                'Trades with PnL over $0',
+                'Trades with PnL over $10',
+                'Trades with PnL over $20'
+            ], name='a')
+        )
         pd.testing.assert_series_equal(
             pf['c'].stats(),
             pf.stats(column='c')
@@ -6766,7 +6859,7 @@ def test_plot_methods(self):
         _ = pf_grouped.plot(column='first', subplots='all')
         _ = pf_grouped.plot(column='a', subplots='all', group_by=False)
         _ = pf_shared.plot(column='a', subplots='all', group_by=False)
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = pf.plot(subplots='all')
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = pf_grouped.plot(subplots='all')
diff --git a/tests/test_records.py b/tests/test_records.py
index bf244c8b..a3256eaa 100644
--- a/tests/test_records.py
+++ b/tests/test_records.py
@@ -5,7 +5,7 @@
 from datetime import datetime
 import pytest
 
-from vectorbt.generic.enums import drawdown_dt
+from vectorbt.generic.enums import range_dt, drawdown_dt
 from vectorbt.portfolio.enums import order_dt, trade_dt, position_dt, log_dt
 
 from tests.utils import record_arrays_close
@@ -31,6 +31,11 @@
     (7, 1, 2, 11, 20),
     (8, 2, 2, 10, 21)
 ], dtype=example_dt)
+records_nosort_arr = np.concatenate((
+    records_arr[0::3],
+    records_arr[1::3],
+    records_arr[2::3]
+))
 
 group_by = pd.Index(['g1', 'g1', 'g2', 'g2'])
 
@@ -44,7 +49,8 @@
 
 records = vbt.records.Records(wrapper, records_arr)
 records_grouped = vbt.records.Records(wrapper_grouped, records_arr)
-records_nosort = records.copy(records_arr=records.records_arr[::-1])
+records_nosort = records.copy(records_arr=records_nosort_arr)
+records_nosort_grouped = vbt.records.Records(wrapper_grouped, records_nosort_arr)
 
 
 # ############# Global ############# #
@@ -177,14 +183,11 @@ def test_is_sorted(self):
 
 mapped_array = records.map_field('some_field1')
 mapped_array_grouped = records_grouped.map_field('some_field1')
+mapped_array_nosort = records_nosort.map_field('some_field1')
+mapped_array_nosort_grouped = records_nosort_grouped.map_field('some_field1')
 mapping = {x: 'test_' + str(x) for x in pd.unique(mapped_array.values)}
 mp_mapped_array = mapped_array.copy(mapping=mapping)
 mp_mapped_array_grouped = mapped_array_grouped.copy(mapping=mapping)
-mapped_array_nosort = mapped_array.copy(
-    col_arr=mapped_array.col_arr[::-1],
-    id_arr=mapped_array.id_arr[::-1],
-    idx_arr=mapped_array.idx_arr[::-1]
-)
 
 
 class TestMappedArray:
@@ -244,25 +247,25 @@ def test_sort(self):
         assert mapped_array.sort().is_sorted(incl_id=True)
         assert mapped_array.sort(incl_id=True).is_sorted(incl_id=True)
         assert mapped_array_nosort.sort().is_sorted()
-        assert not mapped_array_nosort.sort().is_sorted(incl_id=True)
+        assert mapped_array_nosort.sort().is_sorted(incl_id=True)
         assert mapped_array_nosort.sort(incl_id=True).is_sorted(incl_id=True)
 
-    def test_filter_by_mask(self):
+    def test_apply_mask(self):
         mask_a = mapped_array['a'].values >= mapped_array['a'].values.mean()
         np.testing.assert_array_equal(
-            mapped_array['a'].filter_by_mask(mask_a).id_arr,
+            mapped_array['a'].apply_mask(mask_a).id_arr,
             np.array([1, 2])
         )
         mask = mapped_array.values >= mapped_array.values.mean()
-        filtered = mapped_array.filter_by_mask(mask)
+        filtered = mapped_array.apply_mask(mask)
         np.testing.assert_array_equal(
             filtered.id_arr,
             np.array([2, 3, 4, 5, 6])
         )
         np.testing.assert_array_equal(filtered.col_arr, mapped_array.col_arr[mask])
         np.testing.assert_array_equal(filtered.idx_arr, mapped_array.idx_arr[mask])
-        assert mapped_array_grouped.filter_by_mask(mask).wrapper == mapped_array_grouped.wrapper
-        assert mapped_array_grouped.filter_by_mask(mask, group_by=False).wrapper.grouper.group_by is None
+        assert mapped_array_grouped.apply_mask(mask).wrapper == mapped_array_grouped.wrapper
+        assert mapped_array_grouped.apply_mask(mask, group_by=False).wrapper.grouper.group_by is None
 
     def test_map_to_mask(self):
         @njit
@@ -326,7 +329,7 @@ def test_to_pd(self):
             records_arr['col'].tolist() + [2],
             idx_arr=records_arr['idx'].tolist() + [2]
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = mapped_array2.to_pd()
         pd.testing.assert_series_equal(
             mapped_array['a'].to_pd(ignore_index=True),
@@ -598,7 +601,7 @@ def test_nth(self):
             mapped_array.nth(-1),
             pd.Series(np.array([12., 13., 10., np.nan]), index=wrapper.columns).rename('nth')
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = mapped_array.nth(10)
         pd.testing.assert_series_equal(
             mapped_array_grouped.nth(0),
@@ -622,7 +625,7 @@ def test_nth_index(self):
                 index=wrapper.columns
             ).rename('nth_index')
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = mapped_array.nth_index(10)
         pd.testing.assert_series_equal(
             mapped_array_grouped.nth_index(0),
@@ -906,95 +909,105 @@ def test_value_counts(self):
             )
         )
 
-    def test_indexing(self):
+    @pytest.mark.parametrize(
+        "test_nosort",
+        [False, True],
+    )
+    def test_indexing(self, test_nosort):
+        if test_nosort:
+            ma = mapped_array_nosort
+            ma_grouped = mapped_array_nosort_grouped
+        else:
+            ma = mapped_array
+            ma_grouped = mapped_array_grouped
         np.testing.assert_array_equal(
-            mapped_array['a'].id_arr,
+            ma['a'].id_arr,
             np.array([0, 1, 2])
         )
         np.testing.assert_array_equal(
-            mapped_array['a'].col_arr,
+            ma['a'].col_arr,
             np.array([0, 0, 0])
         )
         pd.testing.assert_index_equal(
-            mapped_array['a'].wrapper.columns,
+            ma['a'].wrapper.columns,
             pd.Index(['a'], dtype='object')
         )
         np.testing.assert_array_equal(
-            mapped_array['b'].id_arr,
+            ma['b'].id_arr,
             np.array([3, 4, 5])
         )
         np.testing.assert_array_equal(
-            mapped_array['b'].col_arr,
+            ma['b'].col_arr,
             np.array([0, 0, 0])
         )
         pd.testing.assert_index_equal(
-            mapped_array['b'].wrapper.columns,
+            ma['b'].wrapper.columns,
             pd.Index(['b'], dtype='object')
         )
         np.testing.assert_array_equal(
-            mapped_array[['a', 'a']].id_arr,
+            ma[['a', 'a']].id_arr,
             np.array([0, 1, 2, 0, 1, 2])
         )
         np.testing.assert_array_equal(
-            mapped_array[['a', 'a']].col_arr,
+            ma[['a', 'a']].col_arr,
             np.array([0, 0, 0, 1, 1, 1])
         )
         pd.testing.assert_index_equal(
-            mapped_array[['a', 'a']].wrapper.columns,
+            ma[['a', 'a']].wrapper.columns,
             pd.Index(['a', 'a'], dtype='object')
         )
         np.testing.assert_array_equal(
-            mapped_array[['a', 'b']].id_arr,
+            ma[['a', 'b']].id_arr,
             np.array([0, 1, 2, 3, 4, 5])
         )
         np.testing.assert_array_equal(
-            mapped_array[['a', 'b']].col_arr,
+            ma[['a', 'b']].col_arr,
             np.array([0, 0, 0, 1, 1, 1])
         )
         pd.testing.assert_index_equal(
-            mapped_array[['a', 'b']].wrapper.columns,
+            ma[['a', 'b']].wrapper.columns,
             pd.Index(['a', 'b'], dtype='object')
         )
-        with pytest.raises(Exception) as e_info:
-            _ = mapped_array.iloc[::2, :]  # changing time not supported
+        with pytest.raises(Exception):
+            _ = ma.iloc[::2, :]  # changing time not supported
         pd.testing.assert_index_equal(
-            mapped_array_grouped['g1'].wrapper.columns,
+            ma_grouped['g1'].wrapper.columns,
             pd.Index(['a', 'b'], dtype='object')
         )
-        assert mapped_array_grouped['g1'].wrapper.ndim == 2
-        assert mapped_array_grouped['g1'].wrapper.grouped_ndim == 1
+        assert ma_grouped['g1'].wrapper.ndim == 2
+        assert ma_grouped['g1'].wrapper.grouped_ndim == 1
         pd.testing.assert_index_equal(
-            mapped_array_grouped['g1'].wrapper.grouper.group_by,
+            ma_grouped['g1'].wrapper.grouper.group_by,
             pd.Index(['g1', 'g1'], dtype='object')
         )
         pd.testing.assert_index_equal(
-            mapped_array_grouped['g2'].wrapper.columns,
+            ma_grouped['g2'].wrapper.columns,
             pd.Index(['c', 'd'], dtype='object')
         )
-        assert mapped_array_grouped['g2'].wrapper.ndim == 2
-        assert mapped_array_grouped['g2'].wrapper.grouped_ndim == 1
+        assert ma_grouped['g2'].wrapper.ndim == 2
+        assert ma_grouped['g2'].wrapper.grouped_ndim == 1
         pd.testing.assert_index_equal(
-            mapped_array_grouped['g2'].wrapper.grouper.group_by,
+            ma_grouped['g2'].wrapper.grouper.group_by,
             pd.Index(['g2', 'g2'], dtype='object')
         )
         pd.testing.assert_index_equal(
-            mapped_array_grouped[['g1']].wrapper.columns,
+            ma_grouped[['g1']].wrapper.columns,
             pd.Index(['a', 'b'], dtype='object')
         )
-        assert mapped_array_grouped[['g1']].wrapper.ndim == 2
-        assert mapped_array_grouped[['g1']].wrapper.grouped_ndim == 2
+        assert ma_grouped[['g1']].wrapper.ndim == 2
+        assert ma_grouped[['g1']].wrapper.grouped_ndim == 2
         pd.testing.assert_index_equal(
-            mapped_array_grouped[['g1']].wrapper.grouper.group_by,
+            ma_grouped[['g1']].wrapper.grouper.group_by,
             pd.Index(['g1', 'g1'], dtype='object')
         )
         pd.testing.assert_index_equal(
-            mapped_array_grouped[['g1', 'g2']].wrapper.columns,
+            ma_grouped[['g1', 'g2']].wrapper.columns,
             pd.Index(['a', 'b', 'c', 'd'], dtype='object')
         )
-        assert mapped_array_grouped[['g1', 'g2']].wrapper.ndim == 2
-        assert mapped_array_grouped[['g1', 'g2']].wrapper.grouped_ndim == 2
+        assert ma_grouped[['g1', 'g2']].wrapper.ndim == 2
+        assert ma_grouped[['g1', 'g2']].wrapper.grouped_ndim == 2
         pd.testing.assert_index_equal(
-            mapped_array_grouped[['g1', 'g2']].wrapper.grouper.group_by,
+            ma_grouped[['g1', 'g2']].wrapper.grouper.group_by,
             pd.Index(['g1', 'g1', 'g2', 'g2'], dtype='object')
         )
 
@@ -1023,7 +1036,7 @@ def test_magic(self):
         )
         b_bool = records_arr['some_field2'] > np.mean(records_arr['some_field2'])
         assert a ** a == a ** 2
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = a * a_inv
 
         # binary ops
@@ -1204,6 +1217,16 @@ def test_recarray(self):
         np.testing.assert_array_equal(records['a'].recarray.some_field1, records['a'].values['some_field1'])
         np.testing.assert_array_equal(records.recarray.some_field1, records.values['some_field1'])
 
+    def test_records_readable(self):
+        pd.testing.assert_frame_equal(
+            records.records_readable,
+            pd.DataFrame([
+                [0, 'x', 'a', 10.0, 21.0], [1, 'y', 'a', 11.0, 20.0], [2, 'z', 'a', 12.0, 19.0],
+                [3, 'x', 'b', 13.0, 18.0], [4, 'y', 'b', 14.0, 17.0], [5, 'z', 'b', 13.0, 18.0],
+                [6, 'x', 'c', 12.0, 19.0], [7, 'y', 'c', 11.0, 20.0], [8, 'z', 'c', 10.0, 21.0]
+            ], columns=pd.Index(['Id', 'Date', 'Column', 'some_field1', 'some_field2'], dtype='object'))
+        )
+
     def test_is_sorted(self):
         assert records.is_sorted()
         assert records.is_sorted(incl_id=True)
@@ -1215,19 +1238,19 @@ def test_sort(self):
         assert records.sort().is_sorted(incl_id=True)
         assert records.sort(incl_id=True).is_sorted(incl_id=True)
         assert records_nosort.sort().is_sorted()
-        assert not records_nosort.sort().is_sorted(incl_id=True)
+        assert records_nosort.sort().is_sorted(incl_id=True)
         assert records_nosort.sort(incl_id=True).is_sorted(incl_id=True)
 
-    def test_filter_by_mask(self):
+    def test_apply_mask(self):
         mask_a = records['a'].values['some_field1'] >= records['a'].values['some_field1'].mean()
         record_arrays_close(
-            records['a'].filter_by_mask(mask_a).values,
+            records['a'].apply_mask(mask_a).values,
             np.array([
                 (1, 1, 0, 11., 20.), (2, 2, 0, 12., 19.)
             ], dtype=example_dt)
         )
         mask = records.values['some_field1'] >= records.values['some_field1'].mean()
-        filtered = records.filter_by_mask(mask)
+        filtered = records.apply_mask(mask)
         record_arrays_close(
             filtered.values,
             np.array([
@@ -1235,7 +1258,7 @@ def test_filter_by_mask(self):
                 (5, 2, 1, 13., 18.), (6, 0, 2, 12., 19.)
             ], dtype=example_dt)
         )
-        assert records_grouped.filter_by_mask(mask).wrapper == records_grouped.wrapper
+        assert records_grouped.apply_mask(mask).wrapper == records_grouped.wrapper
 
     def test_map_field(self):
         np.testing.assert_array_equal(
@@ -1246,10 +1269,6 @@ def test_map_field(self):
             records.map_field('some_field1').values,
             np.array([10., 11., 12., 13., 14., 13., 12., 11., 10.])
         )
-        np.testing.assert_array_equal(
-            records.map_field('some_field1', idx_field='col').idx_arr,
-            records_arr['col']
-        )
         assert records_grouped.map_field('some_field1').wrapper == \
                records.map_field('some_field1', group_by=group_by).wrapper
         assert records_grouped.map_field('some_field1', group_by=False).wrapper.grouper.group_by is None
@@ -1267,10 +1286,6 @@ def map_func_nb(record):
             records.map(map_func_nb).values,
             np.array([31., 31., 31., 31., 31., 31., 31., 31., 31.])
         )
-        np.testing.assert_array_equal(
-            records.map(map_func_nb, idx_field='col').idx_arr,
-            records_arr['col']
-        )
         assert records_grouped.map(map_func_nb).wrapper == \
                records.map(map_func_nb, group_by=group_by).wrapper
         assert records_grouped.map(map_func_nb, group_by=False).wrapper.grouper.group_by is None
@@ -1285,10 +1300,6 @@ def test_map_array(self):
             records.map_array(arr).values,
             np.array([31., 31., 31., 31., 31., 31., 31., 31., 31.])
         )
-        np.testing.assert_array_equal(
-            records.map_array(arr, idx_field='col').idx_arr,
-            records_arr['col']
-        )
         assert records_grouped.map_array(arr).wrapper == \
                records.map_array(arr, group_by=group_by).wrapper
         assert records_grouped.map_array(arr, group_by=False).wrapper.grouper.group_by is None
@@ -1336,89 +1347,93 @@ def test_count(self):
             ).rename('count')
         )
 
-    def test_indexing(self):
+    @pytest.mark.parametrize(
+        "test_nosort",
+        [False, True],
+    )
+    def test_indexing(self, test_nosort):
+        if test_nosort:
+            r = records_nosort
+            r_grouped = records_nosort_grouped
+        else:
+            r = records
+            r_grouped = records_grouped
         record_arrays_close(
-            records['a'].values,
+            r['a'].values,
             np.array([
                 (0, 0, 0, 10., 21.), (1, 1, 0, 11., 20.), (2, 2, 0, 12., 19.)
             ], dtype=example_dt)
         )
         pd.testing.assert_index_equal(
-            records['a'].wrapper.columns,
+            r['a'].wrapper.columns,
             pd.Index(['a'], dtype='object')
         )
-        record_arrays_close(
-            records['b'].values,
-            np.array([
-                (3, 0, 0, 13., 18.), (4, 1, 0, 14., 17.), (5, 2, 0, 13., 18.)
-            ], dtype=example_dt)
-        )
         pd.testing.assert_index_equal(
-            records['b'].wrapper.columns,
+            r['b'].wrapper.columns,
             pd.Index(['b'], dtype='object')
         )
         record_arrays_close(
-            records[['a', 'a']].values,
+            r[['a', 'a']].values,
             np.array([
                 (0, 0, 0, 10., 21.), (1, 1, 0, 11., 20.), (2, 2, 0, 12., 19.),
                 (0, 0, 1, 10., 21.), (1, 1, 1, 11., 20.), (2, 2, 1, 12., 19.)
             ], dtype=example_dt)
         )
         pd.testing.assert_index_equal(
-            records[['a', 'a']].wrapper.columns,
+            r[['a', 'a']].wrapper.columns,
             pd.Index(['a', 'a'], dtype='object')
         )
         record_arrays_close(
-            records[['a', 'b']].values,
+            r[['a', 'b']].values,
             np.array([
                 (0, 0, 0, 10., 21.), (1, 1, 0, 11., 20.), (2, 2, 0, 12., 19.),
                 (3, 0, 1, 13., 18.), (4, 1, 1, 14., 17.), (5, 2, 1, 13., 18.)
             ], dtype=example_dt)
         )
         pd.testing.assert_index_equal(
-            records[['a', 'b']].wrapper.columns,
+            r[['a', 'b']].wrapper.columns,
             pd.Index(['a', 'b'], dtype='object')
         )
-        with pytest.raises(Exception) as e_info:
-            _ = records.iloc[::2, :]  # changing time not supported
+        with pytest.raises(Exception):
+            _ = r.iloc[::2, :]  # changing time not supported
         pd.testing.assert_index_equal(
-            records_grouped['g1'].wrapper.columns,
+            r_grouped['g1'].wrapper.columns,
             pd.Index(['a', 'b'], dtype='object')
         )
-        assert records_grouped['g1'].wrapper.ndim == 2
-        assert records_grouped['g1'].wrapper.grouped_ndim == 1
+        assert r_grouped['g1'].wrapper.ndim == 2
+        assert r_grouped['g1'].wrapper.grouped_ndim == 1
         pd.testing.assert_index_equal(
-            records_grouped['g1'].wrapper.grouper.group_by,
+            r_grouped['g1'].wrapper.grouper.group_by,
             pd.Index(['g1', 'g1'], dtype='object')
         )
         pd.testing.assert_index_equal(
-            records_grouped['g2'].wrapper.columns,
+            r_grouped['g2'].wrapper.columns,
             pd.Index(['c', 'd'], dtype='object')
         )
-        assert records_grouped['g2'].wrapper.ndim == 2
-        assert records_grouped['g2'].wrapper.grouped_ndim == 1
+        assert r_grouped['g2'].wrapper.ndim == 2
+        assert r_grouped['g2'].wrapper.grouped_ndim == 1
         pd.testing.assert_index_equal(
-            records_grouped['g2'].wrapper.grouper.group_by,
+            r_grouped['g2'].wrapper.grouper.group_by,
             pd.Index(['g2', 'g2'], dtype='object')
         )
         pd.testing.assert_index_equal(
-            records_grouped[['g1']].wrapper.columns,
+            r_grouped[['g1']].wrapper.columns,
             pd.Index(['a', 'b'], dtype='object')
         )
-        assert records_grouped[['g1']].wrapper.ndim == 2
-        assert records_grouped[['g1']].wrapper.grouped_ndim == 2
+        assert r_grouped[['g1']].wrapper.ndim == 2
+        assert r_grouped[['g1']].wrapper.grouped_ndim == 2
         pd.testing.assert_index_equal(
-            records_grouped[['g1']].wrapper.grouper.group_by,
+            r_grouped[['g1']].wrapper.grouper.group_by,
             pd.Index(['g1', 'g1'], dtype='object')
         )
         pd.testing.assert_index_equal(
-            records_grouped[['g1', 'g2']].wrapper.columns,
+            r_grouped[['g1', 'g2']].wrapper.columns,
             pd.Index(['a', 'b', 'c', 'd'], dtype='object')
         )
-        assert records_grouped[['g1', 'g2']].wrapper.ndim == 2
-        assert records_grouped[['g1', 'g2']].wrapper.grouped_ndim == 2
+        assert r_grouped[['g1', 'g2']].wrapper.ndim == 2
+        assert r_grouped[['g1', 'g2']].wrapper.grouped_ndim == 2
         pd.testing.assert_index_equal(
-            records_grouped[['g1', 'g2']].wrapper.grouper.group_by,
+            r_grouped[['g1', 'g2']].wrapper.grouper.group_by,
             pd.Index(['g1', 'g1', 'g2', 'g2'], dtype='object')
         )
 
@@ -1526,10 +1541,270 @@ def test_stats(self):
         pd.testing.assert_index_equal(stats_df.columns, stats_index)
 
 
+# ############# ranges.py ############# #
+
+ts = pd.DataFrame({
+    'a': [1, -1, 3, -1, 5, -1],
+    'b': [-1, -1, -1, 4, 5, 6],
+    'c': [1, 2, 3, -1, -1, -1],
+    'd': [-1, -1, -1, -1, -1, -1]
+}, index=[
+    datetime(2020, 1, 1),
+    datetime(2020, 1, 2),
+    datetime(2020, 1, 3),
+    datetime(2020, 1, 4),
+    datetime(2020, 1, 5),
+    datetime(2020, 1, 6)
+])
+
+ranges = vbt.Ranges.from_ts(ts, wrapper_kwargs=dict(freq='1 days'))
+ranges_grouped = vbt.Ranges.from_ts(ts, wrapper_kwargs=dict(freq='1 days', group_by=group_by))
+
+
+class TestRanges:
+    def test_mapped_fields(self):
+        for name in range_dt.names:
+            np.testing.assert_array_equal(
+                getattr(ranges, name).values,
+                ranges.values[name]
+            )
+
+    def test_from_ts(self):
+        record_arrays_close(
+            ranges.values,
+            np.array([
+                (0, 0, 0, 1, 1), (1, 0, 2, 3, 1), (2, 0, 4, 5, 1), (3, 1, 3, 5, 0), (4, 2, 0, 3, 1)
+            ], dtype=range_dt)
+        )
+        assert ranges.wrapper.freq == day_dt
+        pd.testing.assert_index_equal(
+            ranges_grouped.wrapper.grouper.group_by,
+            group_by
+        )
+
+    def test_records_readable(self):
+        records_readable = ranges.records_readable
+
+        np.testing.assert_array_equal(
+            records_readable['Range Id'].values,
+            np.array([
+                0, 1, 2, 3, 4
+            ])
+        )
+        np.testing.assert_array_equal(
+            records_readable['Column'].values,
+            np.array([
+                'a', 'a', 'a', 'b', 'c'
+            ])
+        )
+        np.testing.assert_array_equal(
+            records_readable['Start Date'].values,
+            np.array([
+                '2020-01-01T00:00:00.000000000', '2020-01-03T00:00:00.000000000',
+                '2020-01-05T00:00:00.000000000', '2020-01-04T00:00:00.000000000',
+                '2020-01-01T00:00:00.000000000'
+            ], dtype='datetime64[ns]')
+        )
+        np.testing.assert_array_equal(
+            records_readable['End Date'].values,
+            np.array([
+                '2020-01-02T00:00:00.000000000', '2020-01-04T00:00:00.000000000',
+                '2020-01-06T00:00:00.000000000', '2020-01-06T00:00:00.000000000',
+                '2020-01-04T00:00:00.000000000'
+            ], dtype='datetime64[ns]')
+        )
+        np.testing.assert_array_equal(
+            records_readable['Status'].values,
+            np.array([
+                'Closed', 'Closed', 'Closed', 'Open', 'Closed'
+            ])
+        )
+
+    def test_to_mask(self):
+        pd.testing.assert_series_equal(
+            ranges['a'].to_mask(),
+            ts['a'] != -1
+        )
+        pd.testing.assert_frame_equal(
+            ranges.to_mask(),
+            ts != -1
+        )
+        pd.testing.assert_frame_equal(
+            ranges_grouped.to_mask(),
+            pd.DataFrame(
+                [
+                    [True, True],
+                    [False, True],
+                    [True, True],
+                    [True, False],
+                    [True, False],
+                    [True, False]
+                ],
+                index=ts.index,
+                columns=pd.Index(['g1', 'g2'], dtype='object')
+            )
+        )
+
+    def test_duration(self):
+        np.testing.assert_array_equal(
+            ranges['a'].duration.values,
+            np.array([1, 1, 1])
+        )
+        np.testing.assert_array_equal(
+            ranges.duration.values,
+            np.array([1, 1, 1, 3, 3])
+        )
+
+    def test_avg_duration(self):
+        assert ranges['a'].avg_duration() == pd.Timedelta('1 days 00:00:00')
+        pd.testing.assert_series_equal(
+            ranges.avg_duration(),
+            pd.Series(
+                np.array([86400000000000, 259200000000000, 259200000000000, 'NaT'], dtype='timedelta64[ns]'),
+                index=wrapper.columns
+            ).rename('avg_duration')
+        )
+        pd.testing.assert_series_equal(
+            ranges_grouped.avg_duration(),
+            pd.Series(
+                np.array([129600000000000, 259200000000000], dtype='timedelta64[ns]'),
+                index=pd.Index(['g1', 'g2'], dtype='object')
+            ).rename('avg_duration')
+        )
+
+    def test_max_duration(self):
+        assert ranges['a'].max_duration() == pd.Timedelta('1 days 00:00:00')
+        pd.testing.assert_series_equal(
+            ranges.max_duration(),
+            pd.Series(
+                np.array([86400000000000, 259200000000000, 259200000000000, 'NaT'], dtype='timedelta64[ns]'),
+                index=wrapper.columns
+            ).rename('max_duration')
+        )
+        pd.testing.assert_series_equal(
+            ranges_grouped.max_duration(),
+            pd.Series(
+                np.array([259200000000000, 259200000000000], dtype='timedelta64[ns]'),
+                index=pd.Index(['g1', 'g2'], dtype='object')
+            ).rename('max_duration')
+        )
+
+    def test_coverage(self):
+        assert ranges['a'].coverage() == 0.5
+        pd.testing.assert_series_equal(
+            ranges.coverage(),
+            pd.Series(
+                np.array([0.5, 0.5, 0.5, np.nan]),
+                index=ts2.columns
+            ).rename('coverage')
+        )
+        pd.testing.assert_series_equal(
+            ranges.coverage(),
+            ranges.copy(records_arr=np.repeat(ranges.values, 2)).coverage()
+        )
+        pd.testing.assert_series_equal(
+            ranges.copy(records_arr=np.repeat(ranges.values, 2)).coverage(overlapping=True),
+            pd.Series(
+                np.array([1.0, 1.0, 1.0, np.nan]),
+                index=ts2.columns
+            ).rename('coverage')
+        )
+        pd.testing.assert_series_equal(
+            ranges.coverage(normalize=False),
+            pd.Series(
+                np.array([3.0, 3.0, 3.0, np.nan]),
+                index=ts2.columns
+            ).rename('coverage')
+        )
+        pd.testing.assert_series_equal(
+            ranges.copy(records_arr=np.repeat(ranges.values, 2)).coverage(overlapping=True, normalize=False),
+            pd.Series(
+                np.array([3.0, 3.0, 3.0, np.nan]),
+                index=ts2.columns
+            ).rename('coverage')
+        )
+        pd.testing.assert_series_equal(
+            ranges_grouped.coverage(),
+            pd.Series(
+                np.array([0.4166666666666667, 0.25]),
+                index=pd.Index(['g1', 'g2'], dtype='object')
+            ).rename('coverage')
+        )
+        pd.testing.assert_series_equal(
+            ranges_grouped.coverage(),
+            ranges_grouped.copy(records_arr=np.repeat(ranges_grouped.values, 2)).coverage()
+        )
+
+    def test_stats(self):
+        stats_index = pd.Index([
+            'Start', 'End', 'Period', 'Coverage', 'Overlap Coverage',
+            'Total Records', 'Duration: Min', 'Duration: Median', 'Duration: Max',
+            'Duration: Mean', 'Duration: Std'
+        ], dtype='object')
+        pd.testing.assert_series_equal(
+            ranges.stats(),
+            pd.Series([
+                pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-06 00:00:00'),
+                pd.Timedelta('6 days 00:00:00'), pd.Timedelta('3 days 00:00:00'),
+                pd.Timedelta('0 days 00:00:00'), 1.25, pd.Timedelta('2 days 08:00:00'),
+                pd.Timedelta('2 days 08:00:00'), pd.Timedelta('2 days 08:00:00'),
+                pd.Timedelta('2 days 08:00:00'), pd.Timedelta('0 days 00:00:00')
+            ],
+                index=stats_index,
+                name='agg_func_mean'
+            )
+        )
+        pd.testing.assert_series_equal(
+            ranges.stats(column='a'),
+            pd.Series([
+                pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-06 00:00:00'),
+                pd.Timedelta('6 days 00:00:00'), pd.Timedelta('3 days 00:00:00'),
+                pd.Timedelta('0 days 00:00:00'), 3, pd.Timedelta('1 days 00:00:00'),
+                pd.Timedelta('1 days 00:00:00'), pd.Timedelta('1 days 00:00:00'),
+                pd.Timedelta('1 days 00:00:00'), pd.Timedelta('0 days 00:00:00')
+            ],
+                index=stats_index,
+                name='a'
+            )
+        )
+        pd.testing.assert_series_equal(
+            ranges.stats(column='g1', group_by=group_by),
+            pd.Series([
+                pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-06 00:00:00'),
+                pd.Timedelta('6 days 00:00:00'), pd.Timedelta('5 days 00:00:00'),
+                pd.Timedelta('1 days 00:00:00'), 4, pd.Timedelta('1 days 00:00:00'),
+                pd.Timedelta('1 days 00:00:00'), pd.Timedelta('3 days 00:00:00'),
+                pd.Timedelta('1 days 12:00:00'), pd.Timedelta('1 days 00:00:00')
+            ],
+                index=stats_index,
+                name='g1'
+            )
+        )
+        pd.testing.assert_series_equal(
+            ranges['c'].stats(),
+            ranges.stats(column='c')
+        )
+        pd.testing.assert_series_equal(
+            ranges['c'].stats(),
+            ranges.stats(column='c', group_by=False)
+        )
+        pd.testing.assert_series_equal(
+            ranges_grouped['g2'].stats(),
+            ranges_grouped.stats(column='g2')
+        )
+        pd.testing.assert_series_equal(
+            ranges_grouped['g2'].stats(),
+            ranges.stats(column='g2', group_by=group_by)
+        )
+        stats_df = ranges.stats(agg_func=None)
+        assert stats_df.shape == (4, 11)
+        pd.testing.assert_index_equal(stats_df.index, ranges.wrapper.columns)
+        pd.testing.assert_index_equal(stats_df.columns, stats_index)
+
 # ############# drawdowns.py ############# #
 
 
-ts = pd.DataFrame({
+ts2 = pd.DataFrame({
     'a': [2, 1, 3, 1, 4, 1],
     'b': [1, 2, 1, 3, 1, 4],
     'c': [1, 2, 3, 2, 1, 2],
@@ -1543,8 +1818,8 @@ def test_stats(self):
     datetime(2020, 1, 6)
 ])
 
-drawdowns = vbt.Drawdowns.from_ts(ts, freq='1 days')
-drawdowns_grouped = vbt.Drawdowns.from_ts(ts, freq='1 days', group_by=group_by)
+drawdowns = vbt.Drawdowns.from_ts(ts2, wrapper_kwargs=dict(freq='1 days'))
+drawdowns_grouped = vbt.Drawdowns.from_ts(ts2, wrapper_kwargs=dict(freq='1 days', group_by=group_by))
 
 
 class TestDrawdowns:
@@ -1555,17 +1830,31 @@ def test_mapped_fields(self):
                 drawdowns.values[name]
             )
 
+    def test_ts(self):
+        pd.testing.assert_frame_equal(
+            drawdowns.ts,
+            ts2
+        )
+        pd.testing.assert_series_equal(
+            drawdowns['a'].ts,
+            ts2['a']
+        )
+        pd.testing.assert_frame_equal(
+            drawdowns_grouped['g1'].ts,
+            ts2[['a', 'b']]
+        )
+        assert drawdowns.copy(ts=None)['a'].ts is None
+
     def test_from_ts(self):
         record_arrays_close(
             drawdowns.values,
             np.array([
-                (0, 0, 0, 1, 2, 1), (1, 0, 2, 3, 4, 1), (2, 0, 4, 5, 5, 0), (3, 1, 1, 2, 3, 1),
-                (4, 1, 3, 4, 5, 1), (5, 2, 2, 4, 5, 0)
+                (0, 0, 0, 1, 1, 2, 2.0, 1.0, 2.0, 1), (1, 0, 2, 3, 3, 4, 3.0, 1.0, 3.0, 1),
+                (2, 0, 4, 5, 5, 5, 4.0, 1.0, 1.0, 0), (3, 1, 1, 2, 2, 3, 2.0, 1.0, 2.0, 1),
+                (4, 1, 3, 4, 4, 5, 3.0, 1.0, 3.0, 1), (5, 2, 2, 3, 4, 5, 3.0, 1.0, 2.0, 0)
             ], dtype=drawdown_dt)
         )
-        pd.testing.assert_frame_equal(drawdowns.ts, ts)
         assert drawdowns.wrapper.freq == day_dt
-        assert drawdowns.idx_field == 'end_idx'
         pd.testing.assert_index_equal(
             drawdowns_grouped.wrapper.grouper.group_by,
             group_by
@@ -1587,13 +1876,21 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable['Start Date'].values,
+            records_readable['Peak Date'].values,
             np.array([
                 '2020-01-01T00:00:00.000000000', '2020-01-03T00:00:00.000000000',
                 '2020-01-05T00:00:00.000000000', '2020-01-02T00:00:00.000000000',
                 '2020-01-04T00:00:00.000000000', '2020-01-03T00:00:00.000000000'
             ], dtype='datetime64[ns]')
         )
+        np.testing.assert_array_equal(
+            records_readable['Start Date'].values,
+            np.array([
+                '2020-01-02T00:00:00.000000000', '2020-01-04T00:00:00.000000000',
+                '2020-01-06T00:00:00.000000000', '2020-01-03T00:00:00.000000000',
+                '2020-01-05T00:00:00.000000000', '2020-01-04T00:00:00.000000000'
+            ], dtype='datetime64[ns]')
+        )
         np.testing.assert_array_equal(
             records_readable['Valley Date'].values,
             np.array([
@@ -1611,40 +1908,28 @@ def test_records_readable(self):
             ], dtype='datetime64[ns]')
         )
         np.testing.assert_array_equal(
-            records_readable['Status'].values,
+            records_readable['Peak Value'].values,
             np.array([
-                'Recovered', 'Recovered', 'Active', 'Recovered', 'Recovered', 'Active'
+                2., 3., 4., 2., 3., 3.
             ])
         )
-
-    def test_start_value(self):
-        np.testing.assert_array_equal(
-            drawdowns['a'].start_value.values,
-            np.array([2., 3., 4.])
-        )
-        np.testing.assert_array_equal(
-            drawdowns.start_value.values,
-            np.array([2., 3., 4., 2., 3., 3.])
-        )
-
-    def test_valley_value(self):
         np.testing.assert_array_equal(
-            drawdowns['a'].valley_value.values,
-            np.array([1., 1., 1.])
-        )
-        np.testing.assert_array_equal(
-            drawdowns.valley_value.values,
-            np.array([1., 1., 1., 1., 1., 1.])
+            records_readable['Valley Value'].values,
+            np.array([
+                1., 1., 1., 1., 1., 1.
+            ])
         )
-
-    def test_end_value(self):
         np.testing.assert_array_equal(
-            drawdowns['a'].end_value.values,
-            np.array([3., 4., 1.])
+            records_readable['End Value'].values,
+            np.array([
+                2., 3., 1., 2., 3., 2.
+            ])
         )
         np.testing.assert_array_equal(
-            drawdowns.end_value.values,
-            np.array([3., 4., 1., 3., 4., 2.])
+            records_readable['Status'].values,
+            np.array([
+                'Recovered', 'Recovered', 'Active', 'Recovered', 'Recovered', 'Active'
+            ])
         )
 
     def test_drawdown(self):
@@ -1667,8 +1952,8 @@ def test_drawdown(self):
                     [-0.66666669, np.nan, np.nan, np.nan],
                     [-0.75, -0.66666669, -0.66666669, np.nan]
                 ]),
-                index=ts.index,
-                columns=ts.columns
+                index=ts2.index,
+                columns=ts2.columns
             )
         )
 
@@ -1706,63 +1991,122 @@ def test_max_drawdown(self):
             ).rename('max_drawdown')
         )
 
+    def test_recovery_return(self):
+        np.testing.assert_array_almost_equal(
+            drawdowns['a'].recovery_return.values,
+            np.array([1., 2., 0.])
+        )
+        np.testing.assert_array_almost_equal(
+            drawdowns.recovery_return.values,
+            np.array([1., 2., 0., 1., 2., 1.])
+        )
+        pd.testing.assert_frame_equal(
+            drawdowns.recovery_return.to_pd(),
+            pd.DataFrame(
+                np.array([
+                    [np.nan, np.nan, np.nan, np.nan],
+                    [np.nan, np.nan, np.nan, np.nan],
+                    [1.0, np.nan, np.nan, np.nan],
+                    [np.nan, 1.0, np.nan, np.nan],
+                    [2.0, np.nan, np.nan, np.nan],
+                    [0.0, 2.0, 1.0, np.nan]
+                ]),
+                index=ts2.index,
+                columns=ts2.columns
+            )
+        )
+
+    def test_avg_recovery_return(self):
+        assert drawdowns['a'].avg_recovery_return() == 1.0
+        pd.testing.assert_series_equal(
+            drawdowns.avg_recovery_return(),
+            pd.Series(
+                np.array([1.0, 1.5, 1.0, np.nan]),
+                index=wrapper.columns
+            ).rename('avg_recovery_return')
+        )
+        pd.testing.assert_series_equal(
+            drawdowns_grouped.avg_recovery_return(),
+            pd.Series(
+                np.array([1.2, 1.0]),
+                index=pd.Index(['g1', 'g2'], dtype='object')
+            ).rename('avg_recovery_return')
+        )
+
+    def test_max_recovery_return(self):
+        assert drawdowns['a'].max_recovery_return() == 2.0
+        pd.testing.assert_series_equal(
+            drawdowns.max_recovery_return(),
+            pd.Series(
+                np.array([2.0, 2.0, 1.0, np.nan]),
+                index=wrapper.columns
+            ).rename('max_recovery_return')
+        )
+        pd.testing.assert_series_equal(
+            drawdowns_grouped.max_recovery_return(),
+            pd.Series(
+                np.array([2.0, 1.0]),
+                index=pd.Index(['g1', 'g2'], dtype='object')
+            ).rename('max_recovery_return')
+        )
+
     def test_duration(self):
         np.testing.assert_array_almost_equal(
             drawdowns['a'].duration.values,
-            np.array([2., 2., 1.])
+            np.array([1, 1, 1])
         )
         np.testing.assert_array_almost_equal(
             drawdowns.duration.values,
-            np.array([2., 2., 1., 2., 2., 3.])
+            np.array([1, 1, 1, 1, 1, 3])
         )
 
     def test_avg_duration(self):
-        assert drawdowns['a'].avg_duration() == np.timedelta64(144000000000000)
+        assert drawdowns['a'].avg_duration() == pd.Timedelta('1 days 00:00:00')
         pd.testing.assert_series_equal(
             drawdowns.avg_duration(),
             pd.Series(
-                np.array([144000000000000, 172800000000000, 259200000000000, 'NaT'], dtype='timedelta64[ns]'),
+                np.array([86400000000000, 86400000000000, 259200000000000, 'NaT'], dtype='timedelta64[ns]'),
                 index=wrapper.columns
             ).rename('avg_duration')
         )
         pd.testing.assert_series_equal(
             drawdowns_grouped.avg_duration(),
             pd.Series(
-                np.array([155520000000000, 259200000000000], dtype='timedelta64[ns]'),
+                np.array([86400000000000, 259200000000000], dtype='timedelta64[ns]'),
                 index=pd.Index(['g1', 'g2'], dtype='object')
             ).rename('avg_duration')
         )
 
     def test_max_duration(self):
-        assert drawdowns['a'].max_duration() == np.timedelta64(172800000000000)
+        assert drawdowns['a'].max_duration() == pd.Timedelta('1 days 00:00:00')
         pd.testing.assert_series_equal(
             drawdowns.max_duration(),
             pd.Series(
-                np.array([172800000000000, 172800000000000, 259200000000000, 'NaT'], dtype='timedelta64[ns]'),
+                np.array([86400000000000, 86400000000000, 259200000000000, 'NaT'], dtype='timedelta64[ns]'),
                 index=wrapper.columns
             ).rename('max_duration')
         )
         pd.testing.assert_series_equal(
             drawdowns_grouped.max_duration(),
             pd.Series(
-                np.array([172800000000000, 259200000000000], dtype='timedelta64[ns]'),
+                np.array([86400000000000, 259200000000000], dtype='timedelta64[ns]'),
                 index=pd.Index(['g1', 'g2'], dtype='object')
             ).rename('max_duration')
         )
 
     def test_coverage(self):
-        assert drawdowns['a'].coverage() == 0.8333333333333334
+        assert drawdowns['a'].coverage() == 0.5
         pd.testing.assert_series_equal(
             drawdowns.coverage(),
             pd.Series(
-                np.array([0.83333333, 0.66666667, 0.5, 0.]),
-                index=ts.columns
+                np.array([0.5, 0.3333333333333333, 0.5, np.nan]),
+                index=ts2.columns
             ).rename('coverage')
         )
         pd.testing.assert_series_equal(
             drawdowns_grouped.coverage(),
             pd.Series(
-                np.array([0.75, 0.25]),
+                np.array([0.4166666666666667, 0.25]),
                 index=pd.Index(['g1', 'g2'], dtype='object')
             ).rename('coverage')
         )
@@ -1777,13 +2121,33 @@ def test_decline_duration(self):
             np.array([1., 1., 1., 1., 1., 2.])
         )
 
+    def test_recovery_duration(self):
+        np.testing.assert_array_almost_equal(
+            drawdowns['a'].recovery_duration.values,
+            np.array([1, 1, 0])
+        )
+        np.testing.assert_array_almost_equal(
+            drawdowns.recovery_duration.values,
+            np.array([1, 1, 0, 1, 1, 1])
+        )
+
+    def test_recovery_duration_ratio(self):
+        np.testing.assert_array_almost_equal(
+            drawdowns['a'].recovery_duration_ratio.values,
+            np.array([1., 1., 0.])
+        )
+        np.testing.assert_array_almost_equal(
+            drawdowns.recovery_duration_ratio.values,
+            np.array([1., 1., 0., 1., 1., 0.5])
+        )
+
     def test_active_records(self):
         assert isinstance(drawdowns.active, vbt.Drawdowns)
         assert drawdowns.active.wrapper == drawdowns.wrapper
         record_arrays_close(
             drawdowns['a'].active.values,
             np.array([
-                (2, 0, 4, 5, 5, 0)
+                (2, 0, 4, 5, 5, 5, 4., 1., 1., 0)
             ], dtype=drawdown_dt)
         )
         record_arrays_close(
@@ -1793,34 +2157,17 @@ def test_active_records(self):
         record_arrays_close(
             drawdowns.active.values,
             np.array([
-                (2, 0, 4, 5, 5, 0), (5, 2, 2, 4, 5, 0)
+                (2, 0, 4, 5, 5, 5, 4.0, 1.0, 1.0, 0), (5, 2, 2, 3, 4, 5, 3.0, 1.0, 2.0, 0)
             ], dtype=drawdown_dt)
         )
 
-    def test_active_rate(self):
-        assert drawdowns['a'].active_rate() == 0.3333333333333333
-        pd.testing.assert_series_equal(
-            drawdowns.active_rate(),
-            pd.Series(
-                np.array([0.3333333333333333, 0., 1., np.nan]),
-                index=ts.columns
-            ).rename('active_rate')
-        )
-        pd.testing.assert_series_equal(
-            drawdowns_grouped.active_rate(),
-            pd.Series(
-                np.array([0.2, 1.0]),
-                index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('active_rate')
-        )
-
     def test_recovered_records(self):
         assert isinstance(drawdowns.recovered, vbt.Drawdowns)
         assert drawdowns.recovered.wrapper == drawdowns.wrapper
         record_arrays_close(
             drawdowns['a'].recovered.values,
             np.array([
-                (0, 0, 0, 1, 2, 1), (1, 0, 2, 3, 4, 1)
+                (0, 0, 0, 1, 1, 2, 2.0, 1.0, 2.0, 1), (1, 0, 2, 3, 3, 4, 3.0, 1.0, 3.0, 1)
             ], dtype=drawdown_dt)
         )
         record_arrays_close(
@@ -1830,28 +2177,11 @@ def test_recovered_records(self):
         record_arrays_close(
             drawdowns.recovered.values,
             np.array([
-                (0, 0, 0, 1, 2, 1), (1, 0, 2, 3, 4, 1), (3, 1, 1, 2, 3, 1),
-                (4, 1, 3, 4, 5, 1)
+                (0, 0, 0, 1, 1, 2, 2.0, 1.0, 2.0, 1), (1, 0, 2, 3, 3, 4, 3.0, 1.0, 3.0, 1),
+                (3, 1, 1, 2, 2, 3, 2.0, 1.0, 2.0, 1), (4, 1, 3, 4, 4, 5, 3.0, 1.0, 3.0, 1)
             ], dtype=drawdown_dt)
         )
 
-    def test_recovered_rate(self):
-        assert drawdowns['a'].recovered_rate() == 0.6666666666666666
-        pd.testing.assert_series_equal(
-            drawdowns.recovered_rate(),
-            pd.Series(
-                np.array([0.66666667, 1., 0., np.nan]),
-                index=ts.columns
-            ).rename('recovered_rate')
-        )
-        pd.testing.assert_series_equal(
-            drawdowns_grouped.recovered_rate(),
-            pd.Series(
-                np.array([0.8, 0.0]),
-                index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('recovered_rate')
-        )
-
     def test_active_drawdown(self):
         assert drawdowns['a'].active_drawdown() == -0.75
         pd.testing.assert_series_equal(
@@ -1861,7 +2191,7 @@ def test_active_drawdown(self):
                 index=wrapper.columns
             ).rename('active_drawdown')
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             drawdowns_grouped.active_drawdown()
 
     def test_active_duration(self):
@@ -1873,9 +2203,21 @@ def test_active_duration(self):
                 index=wrapper.columns
             ).rename('active_duration')
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             drawdowns_grouped.active_duration()
 
+    def test_active_recovery(self):
+        assert drawdowns['a'].active_recovery() == 0.
+        pd.testing.assert_series_equal(
+            drawdowns.active_recovery(),
+            pd.Series(
+                np.array([0., np.nan, 0.5, np.nan]),
+                index=wrapper.columns
+            ).rename('active_recovery')
+        )
+        with pytest.raises(Exception):
+            drawdowns_grouped.active_recovery()
+
     def test_active_recovery_return(self):
         assert drawdowns['a'].active_recovery_return() == 0.
         pd.testing.assert_series_equal(
@@ -1885,59 +2227,41 @@ def test_active_recovery_return(self):
                 index=wrapper.columns
             ).rename('active_recovery_return')
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             drawdowns_grouped.active_recovery_return()
 
-    def test_recovery_return(self):
-        np.testing.assert_array_almost_equal(
-            drawdowns['a'].recovered.recovery_return.values,
-            np.array([2., 3.])
-        )
-        np.testing.assert_array_almost_equal(
-            drawdowns.recovered.recovery_return.values,
-            np.array([2., 3., 2., 3.])
-        )
-
-    def test_recovery_duration(self):
-        np.testing.assert_array_almost_equal(
-            drawdowns['a'].recovered.recovery_duration.values,
-            np.array([1., 1.])
-        )
-        np.testing.assert_array_almost_equal(
-            drawdowns.recovered.recovery_duration.values,
-            np.array([1., 1., 1., 1.])
-        )
-
-    def test_recovery_duration_ratio(self):
-        np.testing.assert_array_almost_equal(
-            drawdowns['a'].recovered.recovery_duration_ratio.values,
-            np.array([0.5, 0.5])
-        )
-        np.testing.assert_array_almost_equal(
-            drawdowns.recovered.recovery_duration_ratio.values,
-            np.array([0.5, 0.5, 0.5, 0.5])
+    def test_active_recovery_duration(self):
+        assert drawdowns['a'].active_recovery_duration() == pd.Timedelta('0 days 00:00:00')
+        pd.testing.assert_series_equal(
+            drawdowns.active_recovery_duration(),
+            pd.Series(
+                np.array([0, 'NaT', 86400000000000, 'NaT'], dtype='timedelta64[ns]'),
+                index=wrapper.columns
+            ).rename('active_recovery_duration')
         )
+        with pytest.raises(Exception):
+            drawdowns_grouped.active_recovery_duration()
 
     def test_stats(self):
         stats_index = pd.Index([
-            'Start', 'End', 'Period', 'Total Records', 'Total Recovered Drawdowns',
-            'Total Active Drawdowns', 'Active Drawdown [%]', 'Active Duration',
-            'Active Recovery [%]', 'Active Recovery Return [%]',
-            'Active Recovery Duration', 'Max Drawdown [%]', 'Avg Drawdown [%]',
-            'Max Drawdown Duration', 'Avg Drawdown Duration',
-            'Max Recovery Return [%]', 'Avg Recovery Return [%]',
-            'Max Recovery Duration', 'Avg Recovery Duration',
-            'Avg Recovery Duration Ratio'
+            'Start', 'End', 'Period', 'Coverage [%]', 'Total Records',
+            'Total Recovered Drawdowns', 'Total Active Drawdowns',
+            'Active Drawdown [%]', 'Active Duration', 'Active Recovery [%]',
+            'Active Recovery Return [%]', 'Active Recovery Duration',
+            'Max Drawdown [%]', 'Avg Drawdown [%]', 'Max Drawdown Duration',
+            'Avg Drawdown Duration', 'Max Recovery Return [%]',
+            'Avg Recovery Return [%]', 'Max Recovery Duration',
+            'Avg Recovery Duration', 'Avg Recovery Duration Ratio'
         ], dtype='object')
         pd.testing.assert_series_equal(
             drawdowns.stats(),
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-06 00:00:00'),
-                pd.Timedelta('6 days 00:00:00'), 1.5, 1.0, 0.5, 54.166666666666664,
-                pd.Timedelta('2 days 00:00:00'), 25.0, 50.0, pd.Timedelta('0 days 12:00:00'),
-                66.66666666666666, 58.33333333333333, pd.Timedelta('2 days 00:00:00'),
-                pd.Timedelta('2 days 00:00:00'), 300.0, 250.0, pd.Timedelta('1 days 00:00:00'),
-                pd.Timedelta('1 days 00:00:00'), 0.5
+                pd.Timedelta('6 days 00:00:00'), 44.444444444444436, 1.5, 1.0, 0.5,
+                54.166666666666664, pd.Timedelta('2 days 00:00:00'), 25.0, 50.0,
+                pd.Timedelta('0 days 12:00:00'), 66.66666666666666, 58.33333333333333,
+                pd.Timedelta('1 days 00:00:00'), pd.Timedelta('1 days 00:00:00'), 200.0, 150.0,
+                pd.Timedelta('1 days 00:00:00'), pd.Timedelta('1 days 00:00:00'), 1.0
             ],
                 index=stats_index,
                 name='agg_func_mean'
@@ -1947,11 +2271,11 @@ def test_stats(self):
             drawdowns.stats(settings=dict(incl_active=True)),
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-06 00:00:00'),
-                pd.Timedelta('6 days 00:00:00'), 1.5, 1.0, 0.5, 54.166666666666664,
-                pd.Timedelta('2 days 00:00:00'), 25.0, 50.0, pd.Timedelta('0 days 12:00:00'),
-                69.44444444444444, 62.962962962962955, pd.Timedelta('2 days 08:00:00'),
-                pd.Timedelta('2 days 05:20:00'), 300.0, 250.0, pd.Timedelta('1 days 00:00:00'),
-                pd.Timedelta('1 days 00:00:00'), 0.5
+                pd.Timedelta('6 days 00:00:00'), 44.444444444444436, 1.5, 1.0, 0.5,
+                54.166666666666664, pd.Timedelta('2 days 00:00:00'), 25.0, 50.0,
+                pd.Timedelta('0 days 12:00:00'), 69.44444444444444, 62.962962962962955,
+                pd.Timedelta('1 days 16:00:00'), pd.Timedelta('1 days 16:00:00'), 200.0, 150.0,
+                pd.Timedelta('1 days 00:00:00'), pd.Timedelta('1 days 00:00:00'), 1.0
             ],
                 index=stats_index,
                 name='agg_func_mean'
@@ -1961,10 +2285,10 @@ def test_stats(self):
             drawdowns.stats(column='a'),
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-06 00:00:00'),
-                pd.Timedelta('6 days 00:00:00'), 3, 2, 1, 75.0, pd.Timedelta('1 days 00:00:00'),
+                pd.Timedelta('6 days 00:00:00'), 50.0, 3, 2, 1, 75.0, pd.Timedelta('1 days 00:00:00'),
                 0.0, 0.0, pd.Timedelta('0 days 00:00:00'), 66.66666666666666, 58.33333333333333,
-                pd.Timedelta('2 days 00:00:00'), pd.Timedelta('2 days 00:00:00'), 300.0, 250.0,
-                pd.Timedelta('1 days 00:00:00'), pd.Timedelta('1 days 00:00:00'), 0.5
+                pd.Timedelta('1 days 00:00:00'), pd.Timedelta('1 days 00:00:00'), 200.0, 150.0,
+                pd.Timedelta('1 days 00:00:00'), pd.Timedelta('1 days 00:00:00'), 1.0
             ],
                 index=stats_index,
                 name='a'
@@ -1974,17 +2298,17 @@ def test_stats(self):
             drawdowns.stats(column='g1', group_by=group_by),
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-06 00:00:00'),
-                pd.Timedelta('6 days 00:00:00'), 5, 4, 1, 66.66666666666666, 58.33333333333333,
-                pd.Timedelta('2 days 00:00:00'), pd.Timedelta('2 days 00:00:00'), 300.0, 250.0,
-                pd.Timedelta('1 days 00:00:00'), pd.Timedelta('1 days 00:00:00'), 0.5
+                pd.Timedelta('6 days 00:00:00'), 41.66666666666667, 5, 4, 1, 66.66666666666666,
+                58.33333333333333, pd.Timedelta('1 days 00:00:00'), pd.Timedelta('1 days 00:00:00'),
+                200.0, 150.0, pd.Timedelta('1 days 00:00:00'), pd.Timedelta('1 days 00:00:00'), 1.0
             ],
                 index=pd.Index([
-                    'Start', 'End', 'Period', 'Total Records', 'Total Recovered Drawdowns',
-                    'Total Active Drawdowns', 'Max Drawdown [%]', 'Avg Drawdown [%]',
-                    'Max Drawdown Duration', 'Avg Drawdown Duration',
-                    'Max Recovery Return [%]', 'Avg Recovery Return [%]',
-                    'Max Recovery Duration', 'Avg Recovery Duration',
-                    'Avg Recovery Duration Ratio'
+                    'Start', 'End', 'Period', 'Coverage [%]', 'Total Records',
+                    'Total Recovered Drawdowns', 'Total Active Drawdowns',
+                    'Max Drawdown [%]', 'Avg Drawdown [%]', 'Max Drawdown Duration',
+                    'Avg Drawdown Duration', 'Max Recovery Return [%]',
+                    'Avg Recovery Return [%]', 'Max Recovery Duration',
+                    'Avg Recovery Duration', 'Avg Recovery Duration Ratio'
                 ], dtype='object'),
                 name='g1'
             )
@@ -2006,7 +2330,7 @@ def test_stats(self):
             drawdowns.stats(column='g2', group_by=group_by)
         )
         stats_df = drawdowns.stats(agg_func=None)
-        assert stats_df.shape == (4, 20)
+        assert stats_df.shape == (4, 21)
         pd.testing.assert_index_equal(stats_df.index, drawdowns.wrapper.columns)
         pd.testing.assert_index_equal(stats_df.columns, stats_index)
 
@@ -2040,6 +2364,21 @@ def test_mapped_fields(self):
                 orders.values[name]
             )
 
+    def test_close(self):
+        pd.testing.assert_frame_equal(
+            orders.close,
+            close
+        )
+        pd.testing.assert_series_equal(
+            orders['a'].close,
+            close['a']
+        )
+        pd.testing.assert_frame_equal(
+            orders_grouped['g1'].close,
+            close[['a', 'b']]
+        )
+        assert orders.copy(close=None)['a'].close is None
+
     def test_records_readable(self):
         records_readable = orders.records_readable
 
@@ -2129,23 +2468,6 @@ def test_buy_records(self):
             ], dtype=order_dt)
         )
 
-    def test_buy_rate(self):
-        assert orders['a'].buy_rate() == 0.5714285714285714
-        pd.testing.assert_series_equal(
-            orders.buy_rate(),
-            pd.Series(
-                np.array([0.57142857, 0.42857143, 0.57142857, np.nan]),
-                index=close.columns
-            ).rename('buy_rate')
-        )
-        pd.testing.assert_series_equal(
-            orders_grouped.buy_rate(),
-            pd.Series(
-                np.array([0.5, 0.57142857]),
-                index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('buy_rate')
-        )
-
     def test_sell_records(self):
         assert isinstance(orders.sell, vbt.Orders)
         assert orders.sell.wrapper == orders.wrapper
@@ -2171,23 +2493,6 @@ def test_sell_records(self):
             ], dtype=order_dt)
         )
 
-    def test_sell_rate(self):
-        assert orders['a'].sell_rate() == 0.42857142857142855
-        pd.testing.assert_series_equal(
-            orders.sell_rate(),
-            pd.Series(
-                np.array([0.42857143, 0.57142857, 0.42857143, np.nan]),
-                index=close.columns
-            ).rename('sell_rate')
-        )
-        pd.testing.assert_series_equal(
-            orders_grouped.sell_rate(),
-            pd.Series(
-                np.array([0.5, 0.42857143]),
-                index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('sell_rate')
-        )
-
     def test_stats(self):
         stats_index = pd.Index([
             'Start', 'End', 'Period', 'Total Records', 'Total Buy Orders', 'Total Sell Orders',
@@ -2274,6 +2579,21 @@ def test_mapped_fields(self):
                     trades.values[name]
                 )
 
+    def test_close(self):
+        pd.testing.assert_frame_equal(
+            trades.close,
+            close
+        )
+        pd.testing.assert_series_equal(
+            trades['a'].close,
+            close['a']
+        )
+        pd.testing.assert_frame_equal(
+            trades_grouped['g1'].close,
+            close[['a', 'b']]
+        )
+        assert trades.copy(close=None)['a'].close is None
+
     def test_records_arr(self):
         record_arrays_close(
             trades.values,
@@ -2338,7 +2658,7 @@ def test_records_readable(self):
             ], dtype='datetime64[ns]')
         )
         np.testing.assert_array_equal(
-            records_readable['Avg. Entry Price'].values,
+            records_readable['Avg Entry Price'].values,
             np.array([
                 1.0909090909090908, 1.0909090909090908, 6.0, 8.0,
                 1.0909090909090908, 1.0909090909090908, 6.0, 8.0,
@@ -2366,7 +2686,7 @@ def test_records_readable(self):
             ], dtype='datetime64[ns]')
         )
         np.testing.assert_array_equal(
-            records_readable['Avg. Exit Price'].values,
+            records_readable['Avg Exit Price'].values,
             np.array([
                 3.0, 4.0, 7.0, 8.0, 3.0, 4.0, 7.0, 8.0, 3.0, 4.0, 7.0, 8.0, 8.0
             ])
@@ -2418,11 +2738,11 @@ def test_records_readable(self):
     def test_duration(self):
         np.testing.assert_array_almost_equal(
             trades['a'].duration.values,
-            np.array([2., 3., 1., 0.])
+            np.array([2, 3, 1, 1])
         )
         np.testing.assert_array_almost_equal(
             trades.duration.values,
-            np.array([2., 3., 1., 0., 2., 3., 1., 0., 2., 3., 1., 1., 0.])
+            np.array([2, 3, 1, 1, 2, 3, 1, 1, 2, 3, 1, 1, 1])
         )
 
     def test_winning_records(self):
@@ -2452,23 +2772,6 @@ def test_winning_records(self):
             ], dtype=trade_dt)
         )
 
-    def test_win_rate(self):
-        assert trades['a'].win_rate() == 0.75
-        pd.testing.assert_series_equal(
-            trades.win_rate(),
-            pd.Series(
-                np.array([0.75, 0., 0.6, np.nan]),
-                index=close.columns
-            ).rename('win_rate')
-        )
-        pd.testing.assert_series_equal(
-            trades_grouped.win_rate(),
-            pd.Series(
-                np.array([0.375, 0.6]),
-                index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('win_rate')
-        )
-
     def test_losing_records(self):
         assert isinstance(trades.losing, vbt.Trades)
         assert trades.losing.wrapper == trades.wrapper
@@ -2495,21 +2798,21 @@ def test_losing_records(self):
             ], dtype=trade_dt)
         )
 
-    def test_loss_rate(self):
-        assert trades['a'].loss_rate() == 0.25
+    def test_win_rate(self):
+        assert trades['a'].win_rate() == 0.75
         pd.testing.assert_series_equal(
-            trades.loss_rate(),
+            trades.win_rate(),
             pd.Series(
-                np.array([0.25, 1., 0.4, np.nan]),
+                np.array([0.75, 0., 0.6, np.nan]),
                 index=close.columns
-            ).rename('loss_rate')
+            ).rename('win_rate')
         )
         pd.testing.assert_series_equal(
-            trades_grouped.loss_rate(),
+            trades_grouped.win_rate(),
             pd.Series(
-                np.array([0.625, 0.4]),
+                np.array([0.375, 0.6]),
                 index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('loss_rate')
+            ).rename('win_rate')
         )
 
     def test_winning_streak(self):
@@ -2538,7 +2841,7 @@ def test_profit_factor(self):
             trades.profit_factor(),
             pd.Series(
                 np.array([18.9, 0., 2.45853659, np.nan]),
-                index=ts.columns
+                index=ts2.columns
             ).rename('profit_factor')
         )
         pd.testing.assert_series_equal(
@@ -2555,7 +2858,7 @@ def test_expectancy(self):
             trades.expectancy(),
             pd.Series(
                 np.array([0.716, -0.884, 0.3588, np.nan]),
-                index=ts.columns
+                index=ts2.columns
             ).rename('expectancy')
         )
         pd.testing.assert_series_equal(
@@ -2572,7 +2875,7 @@ def test_sqn(self):
             trades.sqn(),
             pd.Series(
                 np.array([1.63415552, -2.13007307, 0.71660403, np.nan]),
-                index=ts.columns
+                index=ts2.columns
             ).rename('sqn')
         )
         pd.testing.assert_series_equal(
@@ -2613,23 +2916,6 @@ def test_long_records(self):
             ], dtype=trade_dt)
         )
 
-    def test_long_rate(self):
-        assert trades['a'].long_rate() == 1.0
-        pd.testing.assert_series_equal(
-            trades.long_rate(),
-            pd.Series(
-                np.array([1., 0., 0.8, np.nan]),
-                index=close.columns
-            ).rename('long_rate')
-        )
-        pd.testing.assert_series_equal(
-            trades_grouped.long_rate(),
-            pd.Series(
-                np.array([0.5, 0.8]),
-                index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('long_rate')
-        )
-
     def test_short_records(self):
         assert isinstance(trades.short, vbt.Trades)
         assert trades.short.wrapper == trades.wrapper
@@ -2652,23 +2938,6 @@ def test_short_records(self):
             ], dtype=trade_dt)
         )
 
-    def test_short_rate(self):
-        assert trades['a'].short_rate() == 0.
-        pd.testing.assert_series_equal(
-            trades.short_rate(),
-            pd.Series(
-                np.array([0., 1., 0.2, np.nan]),
-                index=close.columns
-            ).rename('short_rate')
-        )
-        pd.testing.assert_series_equal(
-            trades_grouped.short_rate(),
-            pd.Series(
-                np.array([0.5, 0.2]),
-                index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('short_rate')
-        )
-
     def test_open_records(self):
         assert isinstance(trades.open, vbt.Trades)
         assert trades.open.wrapper == trades.wrapper
@@ -2691,23 +2960,6 @@ def test_open_records(self):
             ], dtype=trade_dt)
         )
 
-    def test_open_rate(self):
-        assert trades['a'].open_rate() == 0.25
-        pd.testing.assert_series_equal(
-            trades.open_rate(),
-            pd.Series(
-                np.array([0.25, 0.25, 0.2, np.nan]),
-                index=close.columns
-            ).rename('open_rate')
-        )
-        pd.testing.assert_series_equal(
-            trades_grouped.open_rate(),
-            pd.Series(
-                np.array([0.25, 0.2]),
-                index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('open_rate')
-        )
-
     def test_closed_records(self):
         assert isinstance(trades.closed, vbt.Trades)
         assert trades.closed.wrapper == trades.wrapper
@@ -2739,39 +2991,23 @@ def test_closed_records(self):
             ], dtype=trade_dt)
         )
 
-    def test_closed_rate(self):
-        assert trades['a'].closed_rate() == 0.75
-        pd.testing.assert_series_equal(
-            trades.closed_rate(),
-            pd.Series(
-                np.array([0.75, 0.75, 0.8, np.nan]),
-                index=close.columns
-            ).rename('closed_rate')
-        )
-        pd.testing.assert_series_equal(
-            trades_grouped.closed_rate(),
-            pd.Series(
-                np.array([0.75, 0.8]),
-                index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('closed_rate')
-        )
-
     def test_stats(self):
         stats_index = pd.Index([
             'Start', 'End', 'Period', 'First Trade Start', 'Last Trade End',
-            'Total Records', 'Total Long Trades', 'Total Short Trades',
-            'Total Closed Trades', 'Total Open Trades', 'Open Trade P&L',
-            'Win Rate [%]', 'Max Win Streak', 'Max Loss Streak', 'Best Trade [%]',
-            'Worst Trade [%]', 'Avg Winning Trade [%]', 'Avg Losing Trade [%]',
-            'Avg Winning Trade Duration', 'Avg Losing Trade Duration',
-            'Profit Factor', 'Expectancy', 'SQN'
+            'Coverage', 'Overlap Coverage', 'Total Records', 'Total Long Trades',
+            'Total Short Trades', 'Total Closed Trades', 'Total Open Trades',
+            'Open Trade PnL', 'Win Rate [%]', 'Max Win Streak', 'Max Loss Streak',
+            'Best Trade [%]', 'Worst Trade [%]', 'Avg Winning Trade [%]',
+            'Avg Losing Trade [%]', 'Avg Winning Trade Duration',
+            'Avg Losing Trade Duration', 'Profit Factor', 'Expectancy', 'SQN'
         ], dtype='object')
         pd.testing.assert_series_equal(
             trades.stats(),
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-08 00:00:00'),
                 pd.Timedelta('8 days 00:00:00'), pd.Timestamp('2020-01-01 00:00:00'),
-                pd.Timestamp('2020-01-08 00:00:00'), 3.25, 2.0, 1.25, 2.5, 0.75, -0.1,
+                pd.Timestamp('2020-01-08 00:00:00'), pd.Timedelta('5 days 08:00:00'),
+                pd.Timedelta('2 days 00:00:00'), 3.25, 2.0, 1.25, 2.5, 0.75, -0.1,
                 58.333333333333336, 2.0, 1.3333333333333333, 168.38888888888889,
                 -91.08730158730158, 149.25, -86.3670634920635, pd.Timedelta('2 days 00:00:00'),
                 pd.Timedelta('1 days 12:00:00'), np.inf, 0.11705555555555548, 0.18931590012681135
@@ -2785,10 +3021,11 @@ def test_stats(self):
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-08 00:00:00'),
                 pd.Timedelta('8 days 00:00:00'), pd.Timestamp('2020-01-01 00:00:00'),
-                pd.Timestamp('2020-01-08 00:00:00'), 3.25, 2.0, 1.25, 2.5, 0.75, -0.1,
-                45.0, 2.0, 2.3333333333333335, 174.33333333333334, -96.25396825396825,
-                149.25, -42.39781746031746, pd.Timedelta('2 days 00:00:00'), pd.Timedelta('0 days 16:00:00'),
-                7.11951219512195, 0.06359999999999993, 0.07356215977397455
+                pd.Timestamp('2020-01-08 00:00:00'), pd.Timedelta('5 days 08:00:00'),
+                pd.Timedelta('2 days 00:00:00'), 3.25, 2.0, 1.25, 2.5, 0.75, -0.1,
+                58.333333333333336, 2.0, 2.3333333333333335, 174.33333333333334,
+                -96.25396825396825, 149.25, -42.39781746031746, pd.Timedelta('2 days 00:00:00'),
+                pd.Timedelta('1 days 06:00:00'), 7.11951219512195, 0.06359999999999993, 0.07356215977397455
             ],
                 index=stats_index,
                 name='agg_func_mean'
@@ -2799,8 +3036,9 @@ def test_stats(self):
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-08 00:00:00'),
                 pd.Timedelta('8 days 00:00:00'), pd.Timestamp('2020-01-01 00:00:00'),
-                pd.Timestamp('2020-01-08 00:00:00'), 4, 4, 0, 3, 1, -0.16, 100.0, 3,
-                0, 262.0, 14.499999999999998, 149.25, np.nan, pd.Timedelta('2 days 00:00:00'),
+                pd.Timestamp('2020-01-08 00:00:00'), pd.Timedelta('5 days 00:00:00'),
+                pd.Timedelta('2 days 00:00:00'), 4, 4, 0, 3, 1, -0.16, 100.0, 3, 0,
+                262.0, 14.499999999999998, 149.25, np.nan, pd.Timedelta('2 days 00:00:00'),
                 pd.NaT, np.inf, 1.008, 2.181955050824476
             ],
                 index=stats_index,
@@ -2812,15 +3050,26 @@ def test_stats(self):
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-08 00:00:00'),
                 pd.Timedelta('8 days 00:00:00'), pd.Timestamp('2020-01-01 00:00:00'),
-                pd.Timestamp('2020-01-08 00:00:00'), 8, 4, 4, 6, 2, -0.32, 50.0, 3, 3,
-                262.0, -271.3333333333333, 149.25, -156.30555555555557,
-                pd.Timedelta('2 days 00:00:00'), pd.Timedelta('2 days 00:00:00'), 0.895734597156398,
-                -0.058666666666666756, -0.10439051512510047
+                pd.Timestamp('2020-01-08 00:00:00'), pd.Timedelta('5 days 00:00:00'),
+                pd.Timedelta('5 days 00:00:00'), 8, 4, 4, 6, 2, -0.32, 50.0, 3, 3, 262.0,
+                -271.3333333333333, 149.25, -156.30555555555557, pd.Timedelta('2 days 00:00:00'),
+                pd.Timedelta('2 days 00:00:00'), 0.895734597156398, -0.058666666666666756, -0.10439051512510047
             ],
                 index=stats_index,
                 name='g1'
             )
         )
+        pd.testing.assert_index_equal(
+            trades.stats(tags='trades').index,
+            pd.Index([
+                'First Trade Start', 'Last Trade End', 'Total Long Trades',
+                'Total Short Trades', 'Total Closed Trades', 'Total Open Trades',
+                'Open Trade PnL', 'Win Rate [%]', 'Max Win Streak', 'Max Loss Streak',
+                'Best Trade [%]', 'Worst Trade [%]', 'Avg Winning Trade [%]',
+                'Avg Losing Trade [%]', 'Avg Winning Trade Duration',
+                'Avg Losing Trade Duration', 'Profit Factor', 'Expectancy', 'SQN'
+            ], dtype='object')
+        )
         pd.testing.assert_series_equal(
             trades['c'].stats(),
             trades.stats(column='c')
@@ -2838,22 +3087,10 @@ def test_stats(self):
             trades.stats(column='g2', group_by=group_by)
         )
         stats_df = trades.stats(agg_func=None)
-        assert stats_df.shape == (4, 23)
+        assert stats_df.shape == (4, 25)
         pd.testing.assert_index_equal(stats_df.index, trades.wrapper.columns)
         pd.testing.assert_index_equal(stats_df.columns, stats_index)
 
-        pd.testing.assert_index_equal(
-            trades.stats(tags='trades').index,
-            pd.Index([
-                'First Trade Start', 'Last Trade End', 'Total Long Trades',
-                'Total Short Trades', 'Total Closed Trades', 'Total Open Trades',
-                'Open Trade P&L', 'Win Rate [%]', 'Max Win Streak', 'Max Loss Streak',
-                'Best Trade [%]', 'Worst Trade [%]', 'Avg Winning Trade [%]',
-                'Avg Losing Trade [%]', 'Avg Winning Trade Duration',
-                'Avg Losing Trade Duration', 'Profit Factor', 'Expectancy', 'SQN'
-            ], dtype='object')
-        )
-
 
 positions = vbt.Positions.from_trades(trades)
 positions_grouped = vbt.Positions.from_trades(trades_grouped)
@@ -2873,6 +3110,21 @@ def test_mapped_fields(self):
                     positions.values[name]
                 )
 
+    def test_close(self):
+        pd.testing.assert_frame_equal(
+            positions.close,
+            close
+        )
+        pd.testing.assert_series_equal(
+            positions['a'].close,
+            close['a']
+        )
+        pd.testing.assert_frame_equal(
+            positions_grouped['g1'].close,
+            close[['a', 'b']]
+        )
+        assert positions.copy(close=None)['a'].close is None
+
     def test_records_arr(self):
         record_arrays_close(
             positions.values,
@@ -2931,7 +3183,7 @@ def test_records_readable(self):
             ], dtype='datetime64[ns]')
         )
         np.testing.assert_array_equal(
-            records_readable['Avg. Entry Price'].values,
+            records_readable['Avg Entry Price'].values,
             np.array([
                 1.0909090909090908, 6.0, 8.0, 1.0909090909090908, 6.0, 8.0, 1.0909090909090908, 6.0, 7.0, 8.0
             ])
@@ -2953,7 +3205,7 @@ def test_records_readable(self):
             ], dtype='datetime64[ns]')
         )
         np.testing.assert_array_equal(
-            records_readable['Avg. Exit Price'].values,
+            records_readable['Avg Exit Price'].values,
             np.array([
                 3.090909090909091, 7.0, 8.0, 3.090909090909091, 7.0, 8.0, 3.090909090909091, 7.0, 8.0, 8.0
             ])
@@ -2991,44 +3243,26 @@ def test_records_readable(self):
             ])
         )
 
-    def test_coverage(self):
-        assert positions['a'].coverage() == 0.5
-        pd.testing.assert_series_equal(
-            positions.coverage(),
-            pd.Series(
-                np.array([0.5, 0.5, 0.625, 0.]),
-                index=close.columns
-            ).rename('coverage')
-        )
-        pd.testing.assert_series_equal(
-            positions_grouped.coverage(),
-            pd.Series(
-                np.array([0.5, 0.3125]),
-                index=pd.Index(['g1', 'g2'], dtype='object')
-            ).rename('coverage')
-        )
-
     def test_stats(self):
         stats_index = pd.Index([
-            'Start', 'End', 'Period', 'First Position Start', 'Last Position End',
+            'Start', 'End', 'Period', 'First Position Start', 'Last Position End', 'Coverage [%]',
             'Total Records', 'Total Long Positions', 'Total Short Positions',
-            'Total Closed Positions', 'Total Open Positions', 'Open Position P&L',
+            'Total Closed Positions', 'Total Open Positions', 'Open Position PnL',
             'Win Rate [%]', 'Max Win Streak', 'Max Loss Streak',
             'Best Position [%]', 'Worst Position [%]', 'Avg Winning Position [%]',
             'Avg Losing Position [%]', 'Avg Winning Position Duration',
-            'Avg Losing Position Duration', 'Profit Factor', 'Expectancy', 'SQN',
-            'Coverage [%]'
+            'Avg Losing Position Duration', 'Profit Factor', 'Expectancy', 'SQN'
         ], dtype='object')
         pd.testing.assert_series_equal(
             positions.stats(),
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-08 00:00:00'),
                 pd.Timedelta('8 days 00:00:00'), pd.Timestamp('2020-01-01 00:00:00'),
-                pd.Timestamp('2020-01-08 00:00:00'), 2.5, 1.5, 1.0, 1.75, 0.75, -0.1,
+                pd.Timestamp('2020-01-08 00:00:00'), 66.66666666666667, 2.5, 1.5, 1.0, 1.75, 0.75, -0.1,
                 55.55555555555555, 1.3333333333333333, 1.0, 113.38888888888891,
                 -63.031746031746046, 97.00000000000001, -59.714285714285715,
                 pd.Timedelta('2 days 00:00:00'), pd.Timedelta('1 days 12:00:00'), np.inf,
-                0.14955555555555547, -0.006787870702610184, 40.625
+                0.14955555555555547, -0.006787870702610184
             ],
                 index=stats_index,
                 name='agg_func_mean'
@@ -3039,11 +3273,11 @@ def test_stats(self):
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-08 00:00:00'),
                 pd.Timedelta('8 days 00:00:00'), pd.Timestamp('2020-01-01 00:00:00'),
-                pd.Timestamp('2020-01-08 00:00:00'), 2.5, 1.5, 1.0, 1.75, 0.75, -0.1,
-                38.888888888888886, 1.3333333333333333, 2.0, 119.33333333333336,
+                pd.Timestamp('2020-01-08 00:00:00'), 66.66666666666667, 2.5, 1.5, 1.0, 1.75, 0.75, -0.1,
+                55.55555555555555, 1.3333333333333333, 2.0, 119.33333333333336,
                 -68.19841269841271, 97.00000000000001, -26.23809523809524,
-                pd.Timedelta('2 days 00:00:00'), pd.Timedelta('0 days 14:40:00'), 7.11951219512195,
-                0.0748333333333333, 0.036347069097538744, 40.625
+                pd.Timedelta('2 days 00:00:00'), pd.Timedelta('1 days 05:20:00'), 7.11951219512195,
+                0.0748333333333333, 0.036347069097538744
             ],
                 index=stats_index,
                 name='agg_func_mean'
@@ -3054,9 +3288,9 @@ def test_stats(self):
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-08 00:00:00'),
                 pd.Timedelta('8 days 00:00:00'), pd.Timestamp('2020-01-01 00:00:00'),
-                pd.Timestamp('2020-01-08 00:00:00'), 3, 3, 0, 2, 1, -0.16, 100.0, 2, 0,
+                pd.Timestamp('2020-01-08 00:00:00'), 62.5, 3, 3, 0, 2, 1, -0.16, 100.0, 2, 0,
                 179.50000000000003, 14.499999999999998, 97.00000000000001, np.nan,
-                pd.Timedelta('2 days 00:00:00'), pd.NaT, np.inf, 1.512, 2.355140186915887, 50.0
+                pd.Timedelta('2 days 00:00:00'), pd.NaT, np.inf, 1.512, 2.355140186915887
             ],
                 index=stats_index,
                 name='a'
@@ -3067,15 +3301,27 @@ def test_stats(self):
             pd.Series([
                 pd.Timestamp('2020-01-01 00:00:00'), pd.Timestamp('2020-01-08 00:00:00'),
                 pd.Timedelta('8 days 00:00:00'), pd.Timestamp('2020-01-01 00:00:00'),
-                pd.Timestamp('2020-01-08 00:00:00'), 6, 3, 3, 4, 2, -0.32, 50.0, 2, 2,
+                pd.Timestamp('2020-01-08 00:00:00'), 31.25, 6, 3, 3, 4, 2, -0.32, 50.0, 2, 2,
                 179.50000000000003, -187.16666666666669, 97.00000000000001, -103.0,
                 pd.Timedelta('2 days 00:00:00'), pd.Timedelta('2 days 00:00:00'), 0.895734597156398,
-                -0.08800000000000008, -0.08917040366737132, 50.0
+                -0.08800000000000008, -0.08917040366737132
             ],
                 index=stats_index,
                 name='g1'
             )
         )
+        pd.testing.assert_index_equal(
+            positions.stats(tags='positions').index,
+            pd.Index([
+                'First Position Start', 'Last Position End', 'Total Long Positions',
+                'Total Short Positions', 'Total Closed Positions',
+                'Total Open Positions', 'Open Position PnL', 'Win Rate [%]',
+                'Max Win Streak', 'Max Loss Streak', 'Best Position [%]',
+                'Worst Position [%]', 'Avg Winning Position [%]',
+                'Avg Losing Position [%]', 'Avg Winning Position Duration',
+                'Avg Losing Position Duration', 'Profit Factor', 'Expectancy', 'SQN'
+            ], dtype='object')
+        )
         pd.testing.assert_series_equal(
             positions['c'].stats(),
             positions.stats(column='c')
@@ -3097,22 +3343,9 @@ def test_stats(self):
         pd.testing.assert_index_equal(stats_df.index, positions.wrapper.columns)
         pd.testing.assert_index_equal(stats_df.columns, stats_index)
 
-        pd.testing.assert_index_equal(
-            positions.stats(tags='positions').index,
-            pd.Index([
-                'First Position Start', 'Last Position End', 'Total Long Positions',
-                'Total Short Positions', 'Total Closed Positions',
-                'Total Open Positions', 'Open Position P&L', 'Win Rate [%]',
-                'Max Win Streak', 'Max Loss Streak', 'Best Position [%]',
-                'Worst Position [%]', 'Avg Winning Position [%]',
-                'Avg Losing Position [%]', 'Avg Winning Position Duration',
-                'Avg Losing Position Duration', 'Profit Factor', 'Expectancy', 'SQN',
-                'Coverage [%]'
-            ], dtype='object')
-        )
-
 
 # ############# logs.py ############# #
+
 logs = vbt.Portfolio.from_orders(close, size, fees=0.01, log=True, freq='1 days').logs
 logs_grouped = logs.regroup(group_by)
 
@@ -3129,14 +3362,14 @@ def test_records_readable(self):
         records_readable = logs.records_readable
 
         np.testing.assert_array_equal(
-            records_readable[('Context', 'Log Id')].values,
+            records_readable['Log Id'].values,
             np.array([
                 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
                 28, 29, 30, 31
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Context', 'Date')].values,
+            records_readable['Date'].values,
             np.array([
                 '2020-01-01T00:00:00.000000000', '2020-01-02T00:00:00.000000000',
                 '2020-01-03T00:00:00.000000000', '2020-01-04T00:00:00.000000000',
@@ -3157,20 +3390,20 @@ def test_records_readable(self):
             ], dtype='datetime64[ns]')
         )
         np.testing.assert_array_equal(
-            records_readable[('Context', 'Column')].values,
+            records_readable['Column'].values,
             np.array([
                 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'c', 'c', 'c', 'c', 'c',
                 'c', 'c', 'c', 'd', 'd', 'd', 'd', 'd', 'd', 'd', 'd'
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Context', 'Group')].values,
+            records_readable['Group'].values,
             np.array([
                 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Context', 'Cash')].values,
+            records_readable['Cash'].values,
             np.array([
                 100.0, 98.99, 98.788, 101.758, 102.154, 102.154, 96.094, 103.024, 100.0, 100.99, 101.18799999999999,
                 98.15799999999999, 97.75399999999999, 97.75399999999999, 103.69399999999999, 96.624, 100.0, 98.99,
@@ -3179,7 +3412,7 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Context', 'Position')].values,
+            records_readable['Position'].values,
             np.array([
                 0.0, 1.0, 1.1, 0.10000000000000009, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0, -1.1, -0.10000000000000009, 0.0, 0.0,
                 -1.0, 0.0, 0.0, 1.0, 1.1, 0.10000000000000009, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
@@ -3187,14 +3420,14 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Context', 'Debt')].values,
+            records_readable['Debt'].values,
             np.array([
                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.2, 0.10909090909090913, 0.0, 0.0, 6.0, 0.0, 0.0,
                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 7.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Context', 'Free Cash')].values,
+            records_readable['Free Cash'].values,
             np.array([
                 100.0, 98.99, 98.788, 101.758, 102.154, 102.154, 96.094, 103.024, 100.0, 98.99, 98.788,
                 97.93981818181818, 97.754, 97.754, 91.694, 96.624, 100.0, 98.99, 98.788, 101.758, 102.154, 102.154,
@@ -3202,14 +3435,14 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Context', 'Val Price')].values,
+            records_readable['Val Price'].values,
             np.array([
                 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0,
                 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Context', 'Value')].values,
+            records_readable['Value'].values,
             np.array([
                 100.0, 100.99, 102.088, 102.158, 102.154, 102.154, 103.094, 103.024, 100.0, 98.99, 97.88799999999999,
                 97.75799999999998, 97.75399999999999, 97.75399999999999, 96.69399999999999, 96.624, 100.0, 100.99,
@@ -3218,21 +3451,21 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Size')].values,
+            records_readable['Request Size'].values,
             np.array([
                 1.0, 0.1, -1.0, -0.1, np.nan, 1.0, -1.0, 2.0, -1.0, -0.1, 1.0, 0.1, np.nan, -1.0, 1.0, -2.0, 1.0, 0.1,
                 -1.0, -0.1, np.nan, 1.0, -2.0, 2.0, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Price')].values,
+            records_readable['Request Price'].values,
             np.array([
                 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0,
                 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Size Type')].values,
+            records_readable['Request Size Type'].values,
             np.array([
                 'Amount', 'Amount', 'Amount', 'Amount', 'Amount', 'Amount', 'Amount', 'Amount', 'Amount', 'Amount',
                 'Amount', 'Amount', 'Amount', 'Amount', 'Amount', 'Amount', 'Amount', 'Amount', 'Amount', 'Amount',
@@ -3241,7 +3474,7 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Direction')].values,
+            records_readable['Request Direction'].values,
             np.array([
                 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All',
                 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All', 'All',
@@ -3249,28 +3482,28 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Fees')].values,
+            records_readable['Request Fees'].values,
             np.array([
                 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01,
                 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Fixed Fees')].values,
+            records_readable['Request Fixed Fees'].values,
             np.array([
                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Slippage')].values,
+            records_readable['Request Slippage'].values,
             np.array([
                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Min Size')].values,
+            records_readable['Request Min Size'].values,
             np.array([
                 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08,
                 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08, 1e-08,
@@ -3278,7 +3511,7 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Max Size')].values,
+            records_readable['Request Max Size'].values,
             np.array([
                 np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf,
                 np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf, np.inf,
@@ -3286,21 +3519,21 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Rejection Prob')].values,
+            records_readable['Request Rejection Prob'].values,
             np.array([
                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Allow Partial')].values,
+            records_readable['Request Allow Partial'].values,
             np.array([
                 True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True,
                 True, True, True, True, True, True, True, True, True, True, True, True, True, True, True
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Raise Rejection')].values,
+            records_readable['Request Raise Rejection'].values,
             np.array([
                 False, False, False, False, False, False, False, False, False, False, False, False, False, False, False,
                 False, False, False, False, False, False, False, False, False, False, False, False, False, False, False,
@@ -3308,14 +3541,14 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order', 'Log')].values,
+            records_readable['Request Log'].values,
             np.array([
                 True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True,
                 True, True, True, True, True, True, True, True, True, True, True, True, True, True, True
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('New Context', 'Cash')].values,
+            records_readable['New Cash'].values,
             np.array([
                 98.99, 98.788, 101.758, 102.154, 102.154, 96.094, 103.024, 86.864, 100.99, 101.18799999999999,
                 98.15799999999999, 97.75399999999999, 97.75399999999999, 103.69399999999999, 96.624, 112.464, 98.99,
@@ -3324,7 +3557,7 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('New Context', 'Position')].values,
+            records_readable['New Position'].values,
             np.array([
                 1.0, 1.1, 0.10000000000000009, 0.0, 0.0, 1.0, 0.0, 2.0, -1.0, -1.1, -0.10000000000000009, 0.0, 0.0,
                 -1.0, 0.0, -2.0, 1.0, 1.1, 0.10000000000000009, 0.0, 0.0, 1.0, -1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
@@ -3332,14 +3565,14 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('New Context', 'Debt')].values,
+            records_readable['New Debt'].values,
             np.array([
                 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.2, 0.10909090909090913, 0.0, 0.0, 6.0, 0.0, 16.0, 0.0,
                 0.0, 0.0, 0.0, 0.0, 0.0, 7.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('New Context', 'Free Cash')].values,
+            records_readable['New Free Cash'].values,
             np.array([
                 98.99, 98.788, 101.758, 102.154, 102.154, 96.094, 103.024, 86.864, 98.99, 98.788, 97.93981818181818,
                 97.754, 97.754, 91.694, 96.624, 80.464, 98.99, 98.788, 101.758, 102.154, 102.154, 96.094, 95.954,
@@ -3347,14 +3580,14 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('New Context', 'Val Price')].values,
+            records_readable['New Val Price'].values,
             np.array([
                 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0,
                 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('New Context', 'Value')].values,
+            records_readable['New Value'].values,
             np.array([
                 100.0, 100.99, 102.088, 102.158, 102.154, 102.154, 103.094, 103.024, 100.0, 98.99, 97.88799999999999,
                 97.75799999999998, 97.75399999999999, 97.75399999999999, 96.69399999999999, 96.624, 100.0, 100.99,
@@ -3363,21 +3596,21 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order Result', 'Size')].values,
+            records_readable['Result Size'].values,
             np.array([
                 1.0, 0.1, 1.0, 0.1, np.nan, 1.0, 1.0, 2.0, 1.0, 0.1, 1.0, 0.1, np.nan, 1.0, 1.0, 2.0, 1.0, 0.1, 1.0,
                 0.1, np.nan, 1.0, 2.0, 2.0, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order Result', 'Price')].values,
+            records_readable['Result Price'].values,
             np.array([
                 1.0, 2.0, 3.0, 4.0, np.nan, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0, np.nan, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0,
                 4.0, np.nan, 6.0, 7.0, 8.0, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order Result', 'Fees')].values,
+            records_readable['Result Fees'].values,
             np.array([
                 0.01, 0.002, 0.03, 0.004, np.nan, 0.06, 0.07, 0.16, 0.01, 0.002, 0.03, 0.004, np.nan, 0.06, 0.07, 0.16,
                 0.01, 0.002, 0.03, 0.004, np.nan, 0.06, 0.14, 0.16, np.nan, np.nan, np.nan, np.nan, np.nan, np.nan,
@@ -3385,7 +3618,7 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order Result', 'Side')].values,
+            records_readable['Result Side'].values,
             np.array([
                 'Buy', 'Buy', 'Sell', 'Sell', None, 'Buy', 'Sell', 'Buy', 'Sell', 'Sell', 'Buy', 'Buy', None, 'Sell',
                 'Buy', 'Sell', 'Buy', 'Buy', 'Sell', 'Sell', None, 'Buy', 'Sell', 'Buy', None, None, None, None, None,
@@ -3393,7 +3626,7 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order Result', 'Status')].values,
+            records_readable['Result Status'].values,
             np.array([
                 'Filled', 'Filled', 'Filled', 'Filled', 'Ignored', 'Filled', 'Filled', 'Filled', 'Filled', 'Filled',
                 'Filled', 'Filled', 'Ignored', 'Filled', 'Filled', 'Filled', 'Filled', 'Filled', 'Filled', 'Filled',
@@ -3402,7 +3635,7 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order Result', 'Status Info')].values,
+            records_readable['Result Status Info'].values,
             np.array([
                 None, None, None, None, 'SizeNaN', None, None, None, None, None, None, None, 'SizeNaN', None, None,
                 None, None, None, None, None, 'SizeNaN', None, None, None, 'SizeNaN', 'SizeNaN', 'SizeNaN', 'SizeNaN',
@@ -3410,7 +3643,7 @@ def test_records_readable(self):
             ])
         )
         np.testing.assert_array_equal(
-            records_readable[('Order Result', 'Order Id')].values,
+            records_readable['Order Id'].values,
             np.array([
                 0, 1, 2, 3, -1, 4, 5, 6, 7, 8, 9, 10, -1, 11, 12, 13, 14, 15, 16, 17, -1, 18, 19, 20, -1, -1, -1, -1,
                 -1, -1, -1, -1
diff --git a/tests/test_returns.py b/tests/test_returns.py
index e39daa1b..2fe1c0de 100644
--- a/tests/test_returns.py
+++ b/tests/test_returns.py
@@ -39,6 +39,16 @@ def setup_module():
     vbt.settings.caching.enabled = False
     vbt.settings.caching.whitelist = []
     vbt.settings.caching.blacklist = []
+    vbt.settings.returns.defaults = dict(
+        start_value=0.,
+        window=ret.shape[0],
+        minp=1,
+        ddof=1,
+        risk_free=0.01,
+        levy_alpha=2.,
+        required_return=0.1,
+        cutoff=0.05
+    )
 
 
 def teardown_module():
@@ -61,7 +71,7 @@ def test_freq(self):
     def test_ann_factor(self):
         assert ret['a'].vbt.returns(year_freq='365 days').ann_factor == 365
         assert ret.vbt.returns(year_freq='365 days').ann_factor == 365
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             assert pd.Series([1, 2, 3]).vbt.returns(freq=None).ann_factor
 
     def test_from_value(self):
@@ -164,7 +174,7 @@ def test_total_return(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_total(ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_total(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -189,7 +199,7 @@ def test_annualized_return(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_annualized(ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_annualized(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -214,7 +224,7 @@ def test_annualized_volatility(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_annualized_volatility(ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_annualized_volatility(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -239,7 +249,7 @@ def test_calmar_ratio(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_calmar_ratio(ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_calmar_ratio(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -264,7 +274,7 @@ def test_omega_ratio(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_omega_ratio(ret.shape[0], risk_free=0.01, required_return=0.1, minp=1),
+            ret.vbt.returns.rolling_omega_ratio(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -289,7 +299,7 @@ def test_sharpe_ratio(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_sharpe_ratio(ret.shape[0], risk_free=0.01, minp=1),
+            ret.vbt.returns.rolling_sharpe_ratio(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -324,7 +334,7 @@ def test_downside_risk(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_downside_risk(ret.shape[0], required_return=0.1, minp=1),
+            ret.vbt.returns.rolling_downside_risk(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -349,7 +359,7 @@ def test_sortino_ratio(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_sortino_ratio(ret.shape[0], required_return=0.1, minp=1),
+            ret.vbt.returns.rolling_sortino_ratio(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -374,7 +384,7 @@ def test_information_ratio(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_information_ratio(benchmark_rets, ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_information_ratio(benchmark_rets),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -399,7 +409,7 @@ def test_beta(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_beta(benchmark_rets, ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_beta(benchmark_rets),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -424,7 +434,7 @@ def test_alpha(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_alpha(benchmark_rets, ret.shape[0], minp=1, risk_free=0.01),
+            ret.vbt.returns.rolling_alpha(benchmark_rets),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -449,7 +459,7 @@ def test_tail_ratio(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_tail_ratio(ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_tail_ratio(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -474,7 +484,7 @@ def test_value_at_risk(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_value_at_risk(ret.shape[0], minp=1, cutoff=0.05),
+            ret.vbt.returns.rolling_value_at_risk(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -499,7 +509,7 @@ def test_cond_value_at_risk(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_cond_value_at_risk(ret.shape[0], minp=1, cutoff=0.05),
+            ret.vbt.returns.rolling_cond_value_at_risk(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -524,7 +534,7 @@ def test_capture(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_capture(benchmark_rets, ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_capture(benchmark_rets),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -549,7 +559,7 @@ def test_up_capture(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_up_capture(benchmark_rets, ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_up_capture(benchmark_rets),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -574,7 +584,7 @@ def test_down_capture(self):
             )
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_down_capture(benchmark_rets, ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_down_capture(benchmark_rets),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -628,7 +638,7 @@ def test_max_drawdown(self):
             ret.vbt.returns.drawdowns.max_drawdown(fill_value=0.)
         )
         pd.testing.assert_frame_equal(
-            ret.vbt.returns.rolling_max_drawdown(ret.shape[0], minp=1),
+            ret.vbt.returns.rolling_max_drawdown(),
             pd.DataFrame(
                 [
                     [np.nan, np.nan, np.nan],
@@ -676,22 +686,11 @@ def test_stats(self):
         pd.testing.assert_series_equal(
             ret.vbt.returns.stats(),
             pd.Series([
-                pd.Timestamp('2018-01-01 00:00:00'),
-                pd.Timestamp('2018-01-05 00:00:00'),
-                pd.Timedelta('5 days 00:00:00'),
-                106.66666666666667,
-                3.529303946892918e+52,
-                747.9800589021598,
-                73.33333333333333,
-                pd.Timedelta('3 days 00:00:00'),
-                -4.162985893115159, -0.625,
-                np.inf,
-                np.inf,
-                0.25687104876585726,
-                -0.25409565813913854,
-                1.9693400167084374,
-                1.2436594860479807e+51,
-                -0.2291666666666667
+                pd.Timestamp('2018-01-01 00:00:00'), pd.Timestamp('2018-01-05 00:00:00'),
+                pd.Timedelta('5 days 00:00:00'), 106.66666666666667, 3.529303946892918e+52,
+                747.9800589021598, 73.33333333333333, pd.Timedelta('3 days 00:00:00'),
+                -4.926913414772034, -0.625, np.inf, np.inf, 0.25687104876585726, -0.25409565813913854,
+                1.9693400167084374, 1.2436594860479807e+51, -0.2291666666666667
             ],
                 index=stats_index,
                 name='agg_func_mean'
@@ -700,23 +699,10 @@ def test_stats(self):
         pd.testing.assert_series_equal(
             ret.vbt.returns.stats(column='a'),
             pd.Series([
-                pd.Timestamp('2018-01-01 00:00:00'),
-                pd.Timestamp('2018-01-05 00:00:00'),
-                pd.Timedelta('5 days 00:00:00'),
-                400.0,
-                1.0587911840678753e+53,
-                641.7884083645566,
-                np.nan,
-                pd.NaT,
-                29.62100346297954,
-                np.nan,
-                np.inf,
-                np.inf,
-                1.4693345482106241,
-                2.030769230769236,
-                3.5238095238095237,
-                3.730978458143942e+51,
-                0.26249999999999996
+                pd.Timestamp('2018-01-01 00:00:00'), pd.Timestamp('2018-01-05 00:00:00'),
+                pd.Timedelta('5 days 00:00:00'), 400.0, 1.0587911840678753e+53, 641.7884083645566,
+                np.nan, pd.NaT, 29.052280196490333, np.nan, np.inf, np.inf, 1.4693345482106241,
+                2.030769230769236, 3.5238095238095237, 3.730978458143942e+51, 0.26249999999999996
             ],
                 index=stats_index,
                 name='a'
@@ -725,23 +711,10 @@ def test_stats(self):
         pd.testing.assert_series_equal(
             ret.vbt.returns.stats(column='a', settings=dict(freq='10 days', year_freq='200 days')),
             pd.Series([
-                pd.Timestamp('2018-01-01 00:00:00'),
-                pd.Timestamp('2018-01-05 00:00:00'),
-                pd.Timedelta('50 days 00:00:00'),
-                400.0,
-                62400.0,
-                150.23130314433288,
-                np.nan,
-                pd.NaT,
-                6.933752452815364,
-                np.nan,
-                np.inf,
-                np.inf,
-                1.4693345482106241,
-                2.030769230769236,
-                3.5238095238095237,
-                2202.3809523809523,
-                0.26249999999999996
+                pd.Timestamp('2018-01-01 00:00:00'), pd.Timestamp('2018-01-05 00:00:00'),
+                pd.Timedelta('50 days 00:00:00'), 400.0, 62400.0, 150.23130314433288, np.nan, pd.NaT,
+                6.800624405721308, np.nan, np.inf, np.inf, 1.4693345482106241, 2.030769230769236,
+                3.5238095238095237, 2202.3809523809523, 0.26249999999999996
             ],
                 index=stats_index,
                 name='a'
@@ -750,26 +723,11 @@ def test_stats(self):
         pd.testing.assert_series_equal(
             ret.vbt.returns.stats(column='a', settings=dict(benchmark_rets=benchmark_rets)),
             pd.Series([
-                pd.Timestamp('2018-01-01 00:00:00'),
-                pd.Timestamp('2018-01-05 00:00:00'),
-                pd.Timedelta('5 days 00:00:00'),
-                400.0,
-                451.8597134178033,
-                1.0587911840678753e+53,
-                641.7884083645566,
-                np.nan,
-                pd.NaT,
-                29.62100346297954,
-                np.nan,
-                np.inf,
-                np.inf,
-                1.4693345482106241,
-                2.030769230769236,
-                3.5238095238095237,
-                3.730978458143942e+51,
-                0.26249999999999996,
-                86941707686.49066,
-                0.7853755858374825
+                pd.Timestamp('2018-01-01 00:00:00'), pd.Timestamp('2018-01-05 00:00:00'),
+                pd.Timedelta('5 days 00:00:00'), 400.0, 451.8597134178033, 1.0587911840678753e+53,
+                641.7884083645566, np.nan, pd.NaT, 29.052280196490333, np.nan, np.inf, np.inf,
+                1.4693345482106241, 2.030769230769236, 3.5238095238095237, 3.730978458143942e+51,
+                0.26249999999999996, 41819510790.213036, 0.7853755858374825
             ],
                 index=pd.Index([
                     'Start',
diff --git a/tests/test_signals.py b/tests/test_signals.py
index 6df64d7a..38cf91cd 100644
--- a/tests/test_signals.py
+++ b/tests/test_signals.py
@@ -6,6 +6,9 @@
 import pytest
 
 from vectorbt.generic import nb as generic_nb
+from vectorbt.generic.enums import range_dt
+
+from tests.utils import record_arrays_close
 
 seed = 42
 
@@ -125,7 +128,7 @@ def choice_func_nb(from_i, to_i, col, n):
                 name=mask['a'].name
             )
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = pd.Series.vbt.signals.generate((5, 2), choice_func_nb, 1)
         pd.testing.assert_frame_equal(
             pd.DataFrame.vbt.signals.generate(
@@ -505,7 +508,7 @@ def test_clean(self):
                 columns=mask.columns
             )
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = pd.Series.vbt.signals.clean(entries, entries, entries)
 
     def test_generate_random(self):
@@ -518,7 +521,7 @@ def test_generate_random(self):
                 name=mask['a'].name
             )
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = pd.Series.vbt.signals.generate_random((5, 2), n=3)
         pd.testing.assert_frame_equal(
             pd.DataFrame.vbt.signals.generate_random(
@@ -559,7 +562,7 @@ def test_generate_random(self):
                 name=mask['a'].name
             )
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             _ = pd.Series.vbt.signals.generate_random((5, 2), prob=3)
         pd.testing.assert_frame_equal(
             pd.DataFrame.vbt.signals.generate_random(
@@ -591,7 +594,7 @@ def test_generate_random(self):
                 columns=mask.columns
             )
         )
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             pd.DataFrame.vbt.signals.generate_random((5, 3))
         pd.testing.assert_frame_equal(
             pd.DataFrame.vbt.signals.generate_random(
@@ -944,7 +947,7 @@ def test_generate_random_both(self):
             )
         )
         # none
-        with pytest.raises(Exception) as e_info:
+        with pytest.raises(Exception):
             pd.DataFrame.vbt.signals.generate_random((5, 3))
 
     def test_generate_random_exits(self):
@@ -1434,28 +1437,15 @@ def _test_ohlc_stop_exits(**kwargs):
             ]), index=mask.index, columns=mask.columns)
         )
 
-    def test_map_between(self):
-        @njit
-        def distance_map_nb(from_i, to_i, col):
-            return to_i - from_i
-
-        mapped = mask.vbt.signals.map_between(
-            range_map_func_nb=distance_map_nb
-        )
-        np.testing.assert_array_equal(
-            mapped.values,
-            np.array([3., 3.])
-        )
-        np.testing.assert_array_equal(
-            mapped.col_arr,
-            np.array([0, 1])
+    def test_between_ranges(self):
+        ranges = mask.vbt.signals.between_ranges()
+        record_arrays_close(
+            ranges.values,
+            np.array([
+                (0, 0, 0, 3, 1), (1, 1, 1, 4, 1)
+            ], dtype=range_dt)
         )
-        np.testing.assert_array_equal(
-            mapped.idx_arr,
-            np.array([3, 4])
-        )
-        assert mapped.wrapper == mask.vbt.wrapper
-        assert mapped == mask.vbt.signals.distance_mapped()
+        assert ranges.wrapper == mask.vbt.wrapper
 
         mask2 = pd.DataFrame([
             [True, True, True],
@@ -1473,59 +1463,27 @@ def distance_map_nb(from_i, to_i, col):
             [False, False, True]
         ], index=mask.index, columns=mask.columns)
 
-        mapped = mask2.vbt.signals.map_between(
-            range_map_func_nb=distance_map_nb,
-            other=other_mask
-        )
-        np.testing.assert_array_equal(
-            mapped.values,
-            np.array([1., 0., 2., 1., 3., 2.])
-        )
-        np.testing.assert_array_equal(
-            mapped.col_arr,
-            np.array([0, 0, 1, 1, 2, 2])
-        )
-        np.testing.assert_array_equal(
-            mapped.idx_arr,
-            np.array([0, 1, 0, 1, 0, 1])
+        ranges = mask2.vbt.signals.between_ranges(other=other_mask)
+        record_arrays_close(
+            ranges.values,
+            np.array([
+                (0, 0, 0, 1, 1), (1, 0, 1, 1, 1), (2, 1, 0, 2, 1),
+                (3, 1, 1, 2, 1), (4, 2, 0, 3, 1), (5, 2, 1, 3, 1)
+            ], dtype=range_dt)
         )
-        assert mapped.wrapper == mask2.vbt.wrapper
+        assert ranges.wrapper == mask2.vbt.wrapper
 
-        mapped = mask2.vbt.signals.map_between(
-            range_map_func_nb=distance_map_nb,
-            other=other_mask,
-            from_other=True
-        )
-        np.testing.assert_array_equal(
-            mapped.values,
-            np.array([0., 1., 1., 2., 2., 3.])
+        ranges = mask2.vbt.signals.between_ranges(other=other_mask, from_other=True)
+        record_arrays_close(
+            ranges.values,
+            np.array([
+                (0, 0, 1, 1, 1), (1, 0, 1, 2, 1), (2, 1, 1, 2, 1),
+                (3, 1, 1, 3, 1), (4, 2, 1, 3, 1), (5, 2, 1, 4, 1)
+            ], dtype=range_dt)
         )
-        np.testing.assert_array_equal(
-            mapped.col_arr,
-            np.array([0, 0, 1, 1, 2, 2])
-        )
-        np.testing.assert_array_equal(
-            mapped.idx_arr,
-            np.array([1, 2, 2, 3, 3, 4])
-        )
-        assert mapped.wrapper == mask2.vbt.wrapper
-
-        mapped = mask2.vbt.signals.map_between(
-            range_map_func_nb=distance_map_nb,
-            other=other_mask,
-            from_other=True,
-            use_end_idxs=False
-        )
-        np.testing.assert_array_equal(
-            mapped.idx_arr,
-            np.array([1, 1, 1, 1, 1, 1])
-        )
-
-    def test_map_partitions(self):
-        @njit
-        def distance_map_nb(from_i, to_i, col):
-            return to_i - from_i
+        assert ranges.wrapper == mask2.vbt.wrapper
 
+    def test_partition_ranges(self):
         mask2 = pd.DataFrame([
             [False, False, False],
             [True, False, False],
@@ -1534,38 +1492,16 @@ def distance_map_nb(from_i, to_i, col):
             [True, False, True]
         ], index=mask.index, columns=mask.columns)
 
-        mapped = mask2.vbt.signals.map_partitions(
-            range_map_func_nb=distance_map_nb
-        )
-        np.testing.assert_array_equal(
-            mapped.values,
-            np.array([2., 1., 2., 2.])
+        ranges = mask2.vbt.signals.partition_ranges()
+        record_arrays_close(
+            ranges.values,
+            np.array([
+                (0, 0, 1, 3, 1), (1, 0, 4, 4, 0), (2, 1, 2, 4, 1), (3, 2, 3, 4, 0)
+            ], dtype=range_dt)
         )
-        np.testing.assert_array_equal(
-            mapped.col_arr,
-            np.array([0, 0, 1, 2])
-        )
-        np.testing.assert_array_equal(
-            mapped.idx_arr,
-            np.array([2, 4, 3, 4])
-        )
-        assert mapped.wrapper == mask2.vbt.wrapper
-        assert mapped == mask2.vbt.signals.partition_len_mapped()
-
-        mapped = mask2.vbt.signals.map_partitions(
-            range_map_func_nb=distance_map_nb,
-            use_end_idxs=False
-        )
-        np.testing.assert_array_equal(
-            mapped.idx_arr,
-            np.array([1, 4, 2, 3])
-        )
-
-    def test_map_between_partitions(self):
-        @njit
-        def distance_map_nb(from_i, to_i, col):
-            return to_i - from_i
+        assert ranges.wrapper == mask2.vbt.wrapper
 
+    def test_between_partition_ranges(self):
         mask2 = pd.DataFrame([
             [True, False, False],
             [True, True, False],
@@ -1574,189 +1510,14 @@ def distance_map_nb(from_i, to_i, col):
             [False, True, False]
         ], index=mask.index, columns=mask.columns)
 
-        mapped = mask2.vbt.signals.map_between_partitions(
-            range_map_func_nb=distance_map_nb
-        )
-        np.testing.assert_array_equal(
-            mapped.values,
-            np.array([2., 2.])
-        )
-        np.testing.assert_array_equal(
-            mapped.col_arr,
-            np.array([0, 1])
-        )
-        np.testing.assert_array_equal(
-            mapped.idx_arr,
-            np.array([3, 4])
-        )
-        assert mapped.wrapper == mask2.vbt.wrapper
-        assert mapped == mask2.vbt.signals.partition_distance_mapped()
-
-        mapped = mask2.vbt.signals.map_between_partitions(
-            range_map_func_nb=distance_map_nb,
-            use_end_idxs=False
-        )
-        np.testing.assert_array_equal(
-            mapped.idx_arr,
-            np.array([1, 2])
-        )
-
-    def test_map_reduce_between(self):
-        @njit
-        def distance_map_nb(from_i, to_i, col):
-            return to_i - from_i
-
-        @njit
-        def mean_reduce_nb(col, a):
-            return np.nanmean(a)
-
-        mask2 = pd.DataFrame([
-            [True, True, True],
-            [True, True, True],
-            [False, False, False],
-            [False, False, False],
-            [False, False, False]
-        ], index=mask.index, columns=mask.columns)
-
-        other_mask = pd.DataFrame([
-            [False, False, False],
-            [True, False, False],
-            [True, True, False],
-            [False, True, True],
-            [False, False, True]
-        ], index=mask.index, columns=mask.columns)
-
-        assert mask2['a'].vbt.signals.map_reduce_between(
-            range_map_func_nb=distance_map_nb,
-            reduce_func_nb=mean_reduce_nb
-        ) == mask2['a'].vbt.signals.map_between(
-            range_map_func_nb=distance_map_nb
-        ).reduce(reduce_func_nb=mean_reduce_nb)
-        pd.testing.assert_series_equal(
-            mask2.vbt.signals.map_reduce_between(
-                range_map_func_nb=distance_map_nb,
-                reduce_func_nb=mean_reduce_nb
-            ),
-            mask2.vbt.signals.map_between(
-                range_map_func_nb=distance_map_nb
-            ).reduce(reduce_func_nb=mean_reduce_nb, wrap_kwargs=dict(name_or_index="map_reduce_between"))
-        )
-        pd.testing.assert_series_equal(
-            mask2.vbt.signals.map_reduce_between(
-                range_map_func_nb=distance_map_nb,
-                reduce_func_nb=mean_reduce_nb,
-                wrap_kwargs=dict(name_or_index="avg_distance")
-            ),
-            mask2.vbt.signals.avg_distance()
-        )
-        assert mask2['a'].vbt.signals.map_reduce_between(
-            other=other_mask['a'],
-            range_map_func_nb=distance_map_nb,
-            reduce_func_nb=mean_reduce_nb
-        ) == mask2['a'].vbt.signals.map_between(
-            other=other_mask['a'],
-            range_map_func_nb=distance_map_nb
-        ).reduce(reduce_func_nb=mean_reduce_nb)
-        pd.testing.assert_series_equal(
-            mask2.vbt.signals.map_reduce_between(
-                other=other_mask,
-                range_map_func_nb=distance_map_nb,
-                reduce_func_nb=mean_reduce_nb
-            ),
-            mask2.vbt.signals.map_between(
-                other=other_mask,
-                range_map_func_nb=distance_map_nb
-            ).reduce(reduce_func_nb=mean_reduce_nb, wrap_kwargs=dict(name_or_index="map_reduce_between_two"))
-        )
-        assert mask2['a'].vbt.signals.map_reduce_between(
-            other=other_mask['a'],
-            range_map_func_nb=distance_map_nb,
-            reduce_func_nb=mean_reduce_nb,
-            from_other=True
-        ) == mask2['a'].vbt.signals.map_between(
-            other=other_mask['a'],
-            range_map_func_nb=distance_map_nb,
-            from_other=True
-        ).reduce(reduce_func_nb=mean_reduce_nb)
-        pd.testing.assert_series_equal(
-            mask2.vbt.signals.map_reduce_between(
-                other=other_mask,
-                range_map_func_nb=distance_map_nb,
-                reduce_func_nb=mean_reduce_nb,
-                from_other=True
-            ),
-            mask2.vbt.signals.map_between(
-                other=other_mask,
-                range_map_func_nb=distance_map_nb,
-                from_other=True
-            ).reduce(reduce_func_nb=mean_reduce_nb, wrap_kwargs=dict(name_or_index="map_reduce_between_two"))
-        )
-
-    def test_map_reduce_partitions(self):
-        @njit
-        def distance_map_nb(from_i, to_i, col):
-            return to_i - from_i
-
-        @njit
-        def mean_reduce_nb(col, a):
-            return np.nanmean(a)
-
-        mask2 = pd.DataFrame([
-            [False, False, False],
-            [True, False, False],
-            [True, True, False],
-            [False, True, True],
-            [True, False, True]
-        ], index=mask.index, columns=mask.columns)
-
-        assert mask2['a'].vbt.signals.map_reduce_partitions(
-            range_map_func_nb=distance_map_nb,
-            reduce_func_nb=mean_reduce_nb
-        ) == mask2['a'].vbt.signals.map_partitions(
-            range_map_func_nb=distance_map_nb
-        ).reduce(reduce_func_nb=mean_reduce_nb)
-        pd.testing.assert_series_equal(
-            mask2.vbt.signals.map_reduce_partitions(
-                range_map_func_nb=distance_map_nb,
-                reduce_func_nb=mean_reduce_nb
-            ),
-            mask2.vbt.signals.map_partitions(
-                range_map_func_nb=distance_map_nb
-            ).reduce(reduce_func_nb=mean_reduce_nb, wrap_kwargs=dict(name_or_index="map_reduce_partitions"))
-        )
-
-    def test_map_reduce_between_partitions(self):
-        @njit
-        def distance_map_nb(from_i, to_i, col):
-            return to_i - from_i
-
-        @njit
-        def mean_reduce_nb(col, a):
-            return np.nanmean(a)
-
-        mask2 = pd.DataFrame([
-            [True, False, False],
-            [True, True, False],
-            [False, True, True],
-            [True, False, True],
-            [False, True, False]
-        ], index=mask.index, columns=mask.columns)
-
-        assert mask2['a'].vbt.signals.map_reduce_between_partitions(
-            range_map_func_nb=distance_map_nb,
-            reduce_func_nb=mean_reduce_nb
-        ) == mask2['a'].vbt.signals.map_between_partitions(
-            range_map_func_nb=distance_map_nb
-        ).reduce(reduce_func_nb=mean_reduce_nb)
-        pd.testing.assert_series_equal(
-            mask2.vbt.signals.map_reduce_between_partitions(
-                range_map_func_nb=distance_map_nb,
-                reduce_func_nb=mean_reduce_nb
-            ),
-            mask2.vbt.signals.map_between_partitions(
-                range_map_func_nb=distance_map_nb
-            ).reduce(reduce_func_nb=mean_reduce_nb, wrap_kwargs=dict(name_or_index="map_reduce_between_partitions"))
+        ranges = mask2.vbt.signals.between_partition_ranges()
+        record_arrays_close(
+            ranges.values,
+            np.array([
+                (0, 0, 1, 3, 1), (1, 1, 2, 4, 1)
+            ], dtype=range_dt)
         )
+        assert ranges.wrapper == mask2.vbt.wrapper
 
     def test_pos_rank(self):
         pd.testing.assert_series_equal(
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b7ec7a57..ac549a25 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -48,6 +48,8 @@ def teardown_module():
 
 class TestConfig:
     def test_copy_dict(self):
+        assert config.copy_dict(None) == {}
+
         def _init_dict():
             return dict(const=0, lst=[1, 2, 3], dct=dict(const=1, lst=[4, 5, 6]))
 
@@ -142,6 +144,12 @@ def init_config_(**kwargs):
         assert cfg['dct']['lst'] == [4, 5, 6]
 
     def test_update_dict(self):
+        dct = dict(a=1)
+        config.update_dict(dct, None)
+        assert dct == dct
+        config.update_dict(None, dct)
+        assert dct == dct
+
         def init_config_(**kwargs):
             return config.Config(dict(a=0, b=config.Config(dict(c=1), **kwargs)), **kwargs)
 
@@ -955,6 +963,17 @@ def g(cls_or_self):
         assert G.g()
         assert not G().g()
 
+    def test_class_or_instanceproperty(self):
+        class G:
+            @decorators.class_or_instanceproperty
+            def g(cls_or_self):
+                if isinstance(cls_or_self, type):
+                    return True  # class
+                return False  # instance
+
+        assert G.g
+        assert not G().g
+
     def test_custom_property(self):
         class G:
             @decorators.custom_property(some='key')
@@ -1510,6 +1529,12 @@ def c(self):
 # ############# checks.py ############# #
 
 class TestChecks:
+    def test_is_np_array(self):
+        assert not checks.is_np_array(0)
+        assert checks.is_np_array(np.array([0]))
+        assert not checks.is_np_array(pd.Series([1, 2, 3]))
+        assert not checks.is_np_array(pd.DataFrame([1, 2, 3]))
+
     def test_is_pandas(self):
         assert not checks.is_pandas(0)
         assert not checks.is_pandas(np.array([0]))
@@ -1534,6 +1559,18 @@ def test_is_array(self):
         assert checks.is_any_array(pd.Series([1, 2, 3]))
         assert checks.is_any_array(pd.DataFrame([1, 2, 3]))
 
+    def test_is_sequence(self):
+        assert checks.is_sequence([1, 2, 3])
+        assert checks.is_sequence('123')
+        assert not checks.is_sequence(0)
+        assert not checks.is_sequence(dict(a=2).items())
+
+    def test_is_iterable(self):
+        assert checks.is_iterable([1, 2, 3])
+        assert checks.is_iterable('123')
+        assert not checks.is_iterable(0)
+        assert checks.is_iterable(dict(a=2).items())
+
     def test_is_numba_func(self):
         def test_func(x):
             return x
@@ -1794,13 +1831,13 @@ def test_assert_not_none(self):
             checks.assert_not_none(None)
 
     def test_assert_type(self):
-        checks.assert_type(0, int)
-        checks.assert_type(np.zeros(1), (np.ndarray, pd.Series))
-        checks.assert_type(pd.Series([1, 2, 3]), (np.ndarray, pd.Series))
+        checks.assert_instance_of(0, int)
+        checks.assert_instance_of(np.zeros(1), (np.ndarray, pd.Series))
+        checks.assert_instance_of(pd.Series([1, 2, 3]), (np.ndarray, pd.Series))
         with pytest.raises(Exception):
-            checks.assert_type(pd.DataFrame([1, 2, 3]), (np.ndarray, pd.Series))
+            checks.assert_instance_of(pd.DataFrame([1, 2, 3]), (np.ndarray, pd.Series))
 
-    def test_assert_subclass(self):
+    def test_assert_subclass_of(self):
         class A:
             pass
 
@@ -1810,17 +1847,17 @@ class B(A):
         class C(B):
             pass
 
-        checks.assert_subclass(B, A)
-        checks.assert_subclass(C, B)
-        checks.assert_subclass(C, A)
+        checks.assert_subclass_of(B, A)
+        checks.assert_subclass_of(C, B)
+        checks.assert_subclass_of(C, A)
         with pytest.raises(Exception):
-            checks.assert_subclass(A, B)
+            checks.assert_subclass_of(A, B)
 
     def test_assert_type_equal(self):
         checks.assert_type_equal(0, 1)
         checks.assert_type_equal(np.zeros(1), np.empty(1))
         with pytest.raises(Exception):
-            checks.assert_type(0, np.zeros(1))
+            checks.assert_instance_of(0, np.zeros(1))
 
     def test_assert_dtype(self):
         checks.assert_dtype(np.zeros(1), np.float_)
@@ -2163,6 +2200,35 @@ def test_to_mapping(self):
         assert mapping.to_mapping(pd.Index(['Attr1', 'Attr2'])) == {0: 'Attr1', 1: 'Attr2'}
         assert mapping.to_mapping(pd.Series(['Attr1', 'Attr2'])) == {0: 'Attr1', 1: 'Attr2'}
 
+    def test_apply_mapping(self):
+        assert mapping.apply_mapping('Attr1', mapping_like=Enum, reverse=True) == 0
+        with pytest.raises(Exception):
+            _ = mapping.apply_mapping('Attr3', mapping_like=Enum, reverse=True)
+        assert mapping.apply_mapping('attr1', mapping_like=Enum, reverse=True, ignore_case=True) == 0
+        with pytest.raises(Exception):
+            _ = mapping.apply_mapping('attr1', mapping_like=Enum, reverse=True, ignore_case=False)
+        assert mapping.apply_mapping('Attr_1', mapping_like=Enum, reverse=True, ignore_underscores=True) == 0
+        with pytest.raises(Exception):
+            _ = mapping.apply_mapping('Attr_1', mapping_like=Enum, reverse=True, ignore_underscores=False)
+        assert mapping.apply_mapping(
+            'attr_1', mapping_like=Enum, reverse=True, ignore_case=True,
+            ignore_underscores=True) == 0
+        with pytest.raises(Exception):
+            _ = mapping.apply_mapping(
+                'attr_1', mapping_like=Enum, reverse=True, ignore_case=True,
+                ignore_underscores=False)
+        assert mapping.apply_mapping(np.array([1]), mapping_like={1: 'hello'})[0] == 'hello'
+        assert mapping.apply_mapping(np.array([1]), mapping_like={1.: 'hello'})[0] == 'hello'
+        assert mapping.apply_mapping(np.array([1.]), mapping_like={1: 'hello'})[0] == 'hello'
+        assert mapping.apply_mapping(np.array([True]), mapping_like={1: 'hello'})[0] == 'hello'
+        assert mapping.apply_mapping(np.array([True]), mapping_like={True: 'hello'})[0] == 'hello'
+        with pytest.raises(Exception):
+            _ = mapping.apply_mapping(np.array([True]), mapping_like={'world': 'hello'})
+        with pytest.raises(Exception):
+            _ = mapping.apply_mapping(np.array([1]), mapping_like={'world': 'hello'})
+        assert mapping.apply_mapping(np.array(['world']), mapping_like={'world': 'hello'})[0] == 'hello'
+
+
 # ############# enum.py ############# #
 
 
@@ -2170,26 +2236,23 @@ class TestEnum:
     def test_map_enum_fields(self):
         assert enum.map_enum_fields(0, Enum) == 0
         assert enum.map_enum_fields(10, Enum) == 10
-        assert enum.map_enum_fields(10., Enum) == 10.
+        with pytest.raises(Exception):
+            _ = enum.map_enum_fields(10., Enum)
         assert enum.map_enum_fields('Attr1', Enum) == 0
         assert enum.map_enum_fields('attr1', Enum) == 0
         with pytest.raises(Exception):
             _ = enum.map_enum_fields('hello', Enum)
-        assert enum.map_enum_fields('attr1', Enum, ignore_other_types=False) == 0
-        with pytest.raises(Exception):
-            _ = enum.map_enum_fields(0, Enum, ignore_other_types=False)
-        with pytest.raises(Exception):
-            _ = enum.map_enum_fields(0., Enum, ignore_other_types=False)
+        assert enum.map_enum_fields('attr1', Enum) == 0
         assert enum.map_enum_fields(('attr1', 'attr2'), Enum) == (0, 1)
         assert enum.map_enum_fields([['attr1', 'attr2']], Enum) == [[0, 1]]
         np.testing.assert_array_equal(
             enum.map_enum_fields(np.array([]), Enum),
             np.array([])
         )
-        np.testing.assert_array_equal(
-            enum.map_enum_fields(np.array([[0., 1.]]), Enum),
-            np.array([[0., 1.]])
-        )
+        with pytest.raises(Exception):
+            _ = enum.map_enum_fields(np.array([[0., 1.]]), Enum)
+        with pytest.raises(Exception):
+            _ = enum.map_enum_fields(np.array([[False, True]]), Enum)
         np.testing.assert_array_equal(
             enum.map_enum_fields(np.array([[0, 1]]), Enum),
             np.array([[0, 1]])
@@ -2204,10 +2267,10 @@ def test_map_enum_fields(self):
             enum.map_enum_fields(pd.Series([]), Enum),
             pd.Series([])
         )
-        pd.testing.assert_series_equal(
-            enum.map_enum_fields(pd.Series([0., 1.]), Enum),
-            pd.Series([0., 1.])
-        )
+        with pytest.raises(Exception):
+            _ = enum.map_enum_fields(pd.Series([0., 1.]), Enum)
+        with pytest.raises(Exception):
+            _ = enum.map_enum_fields(pd.Series([False, True]), Enum)
         pd.testing.assert_series_equal(
             enum.map_enum_fields(pd.Series([0, 1]), Enum),
             pd.Series([0, 1])
@@ -2222,10 +2285,8 @@ def test_map_enum_fields(self):
             enum.map_enum_fields(pd.DataFrame([]), Enum),
             pd.DataFrame([])
         )
-        pd.testing.assert_frame_equal(
-            enum.map_enum_fields(pd.DataFrame([[0., 1.]]), Enum),
-            pd.DataFrame([[0., 1.]])
-        )
+        with pytest.raises(Exception):
+            _ = enum.map_enum_fields(pd.DataFrame([[0., 1.]]), Enum)
         pd.testing.assert_frame_equal(
             enum.map_enum_fields(pd.DataFrame([[0, 1]]), Enum),
             pd.DataFrame([[0, 1]])
@@ -2235,11 +2296,9 @@ def test_map_enum_fields(self):
             pd.DataFrame([[0, 1]])
         )
         pd.testing.assert_frame_equal(
-            enum.map_enum_fields(pd.DataFrame([['attr1', 0]]), Enum),
-            pd.DataFrame([[0, 0]])
+            enum.map_enum_fields(pd.DataFrame([[0, 'attr2']]), Enum),
+            pd.DataFrame([[0, 1]])
         )
-        with pytest.raises(Exception):
-            _ = enum.map_enum_fields(pd.DataFrame([['attr1', 0]]), Enum, ignore_other_types=False)
 
     def test_map_enum_values(self):
         assert enum.map_enum_values(0, Enum) == 'Attr1'
@@ -2248,10 +2307,7 @@ def test_map_enum_values(self):
             _ = enum.map_enum_values(-2, Enum)
         assert enum.map_enum_values((0, 1, 'Attr3'), Enum) == ('Attr1', 'Attr2', 'Attr3')
         assert enum.map_enum_values([[0, 1, 'Attr3']], Enum) == [['Attr1', 'Attr2', 'Attr3']]
-        assert enum.map_enum_values(0, Enum, ignore_other_types=False) == 'Attr1'
-        assert enum.map_enum_values(0., Enum, ignore_other_types=False) == 'Attr1'
-        with pytest.raises(Exception):
-            _ = enum.map_enum_values('hello', Enum, ignore_other_types=False)
+        assert enum.map_enum_values('hello', Enum) == 'hello'
         np.testing.assert_array_equal(
             enum.map_enum_values(np.array([]), Enum),
             np.array([])
@@ -2306,8 +2362,6 @@ def test_map_enum_values(self):
             enum.map_enum_values(pd.DataFrame([[0, 'Attr2']]), Enum),
             pd.DataFrame([['Attr1', 'Attr2']])
         )
-        with pytest.raises(Exception):
-            _ = enum.map_enum_values(pd.DataFrame([[0, 'Attr2']]), Enum, ignore_other_types=False)
 
 
 # ############# params.py ############# #