| Overall Statistics |
|
Total Trades 49 Average Win 5.30% Average Loss -2.66% Compounding Annual Return 12.054% Drawdown 19.700% Expectancy 1.620 Net Profit 168.337% Sharpe Ratio 0.608 Sortino Ratio 0.479 Probabilistic Sharpe Ratio 16.214% Loss Rate 12% Win Rate 88% Profit-Loss Ratio 1.99 Alpha 0.026 Beta 0.63 Annual Standard Deviation 0.119 Annual Variance 0.014 Information Ratio -0.009 Tracking Error 0.091 Treynor Ratio 0.115 Total Fees $49.00 Estimated Strategy Capacity $74000000.00 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 1.50% |
# ----< Global Imports >----
from AlgorithmImports import *
import numpy as np
import scipy
from scipy.signal import find_peaks
import pandas as pd
import timeit
def find_levels(
price: np.array, atr: float, # Log closing price, and log atr
first_w: float = 0.1, # The last price point has only 10% of the weight
atr_mult: float = 1.0,
prom_thresh: float = 0.1
):
# Setup weights
last_w = 1.0 # the newest data has the most weight
w_step = (last_w - first_w) / len(price)
weights = first_w + np.arange(len(price)) * w_step
weights[weights < 0] = 0.0
# Get kernel of price.
kernal = scipy.stats.gaussian_kde(price, bw_method=atr*atr_mult, weights=weights)
# Construct market profile
min_v = np.min(price)
max_v = np.max(price)
step = (max_v - min_v) / 200
price_range = np.arange(min_v, max_v, step)
pdf = kernal(price_range) # Market profile
# Find significant peaks in the market profile
pdf_max = np.max(pdf)
prom_min = pdf_max * prom_thresh
peaks, props = scipy.signal.find_peaks(pdf, prominence=prom_min)
levels = []
for peak in peaks:
levels.append(price_range[peak])
return levels, peaks, props, price_range, pdf, weights
# ----< Global Imports >----
from AlgorithmImports import *
from harmonics import *
from levels import *
from trendlines import *
class JumpingBlackWhale(QCAlgorithm):
def Initialize(self):
# Backtest Settings
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2023, 9, 1)
self.SetCash(10000)
self.lookback = 30
# Instrument
self.symbol = 'SPY'
self.AddEquity(self.symbol, Resolution.Minute)
# Indicators
self.SMA = SimpleMovingAverage(self.symbol, 21)
self.SMA_fast = SimpleMovingAverage(self.symbol, 20)
self.STR = SuperTrend(20, 2, MovingAverageType.Wilders)
self.ATR = AverageTrueRange(self.symbol, 20)
# Consolidator
self.cons = TradeBarConsolidator(timedelta(days=1))
self.cons.DataConsolidated += self.consolidation_update
self.SubscriptionManager.AddConsolidator(self.symbol, self.cons)
# Register Indicator
self.RegisterIndicator(self.symbol, self.SMA_fast, self.cons)
self.RegisterIndicator(self.symbol, self.STR, self.cons)
self.RegisterIndicator(self.symbol, self.ATR, self.cons)
# Rolling Windows
self.close_prices_window = RollingWindow[float](self.lookback)
self.high_prices_window = RollingWindow[float](self.lookback)
self.low_prices_window = RollingWindow[float](self.lookback)
self.index_window = RollingWindow[DateTime](self.lookback)
self.closest_levels = RollingWindow[float](self.lookback)
# Initialize Modules
self.trend = Trendline_Identification(self.lookback)
self.harm = HarmonicsIdentification(self, 0.01)
# Helpers
self.series = Chart("Chart")
self.circle = ScatterMarkerSymbol.Circle
# Adding formatted series
self.series.AddSeries(
Series("Buy", SeriesType.Scatter, "$", Color.Green, self.circle)
)
self.series.AddSeries(
Series("Sell", SeriesType.Scatter, "$", Color.Red, self.circle)
)
self.series.AddSeries(
Series("Harmonic Sell", SeriesType.Scatter, "$", Color.Yellow, self.circle)
)
self.AddChart(self.series)
self.warmup = 0
self.quantity = 0
self.stop = None
self.bought = None
self.sell_allowed = False
def find_closest_number(self, array, x, atr):
closest_number = float('-inf')
new_array = []
for i in array:
new_array.append(i+atr)
array = new_array
for num in array:
if num < x and num > closest_number:
closest_number = num
if closest_number == float('-inf'):
return x
return closest_number
def OnData(self, data: Slice):
pass
def OnOrderEvent(self, orderEvent: object):
self.quantity += orderEvent.FillQuantity
if orderEvent.FillQuantity < 0:
self.Plot("Chart", "Sell", orderEvent.FillPrice)
def consolidation_update(self, sender: Any, bar: object) -> None:
# Populate rolling windows
self.index_window.Add(bar.EndTime)
self.close_prices_window.Add(bar.Close)
self.high_prices_window.Add(bar.High)
self.low_prices_window.Add(bar.Low)
self.Plot("Chart", "Price", bar.Close)
if self.warmup > self.lookback:
# ----< Initialize Market-View Modules >----
list_index = []
list_close = []
list_low = []
list_high = []
# Convert rolling windows to lists
for x in self.index_window:
list_index.append(x)
for x in self.close_prices_window:
list_close.append(x)
for x in self.low_prices_window:
list_low.append(x)
for x in self.high_prices_window:
list_high.append(x)
# Convert all lists to pd.Series
df_index = pd.Series(list_index[::-1])
df_close = pd.Series(list_close[::-1])
df_low = pd.Series(list_low[::-1])
df_high = pd.Series(list_high[::-1])
# Convert to one dataframe
subset = pd.DataFrame({'High': df_high, 'Close': df_close, 'Low': df_low})
subset.index = df_index
# Modules
pattern, best_fit_coefs, support_coefs, resist_coefs = self.trend.fit_trendlines(df_close)
UTILS_, XABCD_, best_pat, ad = self.harm.harmonics(subset)
levels, peaks, props, price_range, pdf, weights = find_levels(np.array(subset['Close']), self.ATR.Current.Value)
self.harm.call_after_plotting(ad)
self.closest_levels.Add(self.find_closest_number(levels, bar.Close, self.ATR.Current.Value))
if best_pat and "Bullish" in best_pat:
self.Plot("Chart", "Buy", bar.Close)
self.SetHoldings(self.symbol, 1)
self.sell_allowed = False
if best_pat and "Bearish" in best_pat and "bullish" in pattern or "rising" in pattern:
self.sell_allowed = True
self.Plot("Chart", "Harmonic Sell", bar.Close)
# self.SetHoldings(self.symbol, 0)
if self.sell_allowed and self.stop and bar.Close < self.stop:
self.SetHoldings(self.symbol, 0)
self.Plot("Chart", "Sell", bar.Close)
self.sell_allowed = False
if self.stop:
self.Plot("Chart", "stop", self.stop)
if self.closest_levels.IsReady:
self.stop = self.closest_levels[0]
if self.closest_levels[1] < self.closest_levels[0]:
self.stop = self.closest_levels[1]
if bar.Close < self.stop:
self.stop = self.ATR.Current.Value
self.warmup = self.warmup + 1# ----< Global Imports >----
from AlgorithmImports import *
from dataclasses import dataclass
from collections import deque
from typing import Union
import pandas as pd
import numpy as np
import itertools
class HarmonicsIdentification():
@dataclass
class XABCD_RATIOS:
"""
Description:
A data class for holding information about the pattern
"""
XB: Union[float, list, None]
AC: Union[float, list, None]
DB: Union[float, list, None]
XD: Union[float, list, None]
name: str
@dataclass
class XABCD:
"""
Description:
A data class which holds information of the current pattern, its values
"""
# Index on the chart, that is accepted by mplfinance
# If there currently is 100 candles on the chart, and D is the
# last data point, then D will be 100.
X: float
D: float
C: float
B: float
A: float
# Prices of each point
X_price: float
D_price: float
C_price: float
B_price: float
A_price: float
# Retracement ratios between key points
DB_ratio: float
AC_ratio: float
XB_ratio: float
XD_ratio: float
def __init__(self):
# Reset Variables with each initialization
self.X = None
self.D = None
self.C = None
self.B = None
self.A = None
self.X_price = None
self.D_price = None
self.C_price = None
self.B_price = None
self.A_price = None
self.DB_ratio = None
self.AC_ratio = None
self.XB_ratio = None
self.XD_ratio = None
@dataclass
class UTILS:
"""
Description:
A data class which holds data that is used for plotting
"""
bottoms_ratios: list # List of indexes for the bottom points
tops_ratios: list # List of indexes for the upper points
# Tuples that are accepted by mplfinance for plotting a tline
indexes_pairs: tuple
top_ratios_pairs: tuple
bottoms_ratios_pairs: tuple
xd_line: tuple
def __init__(self):
# Reset Variables with each initialization
self.bottoms_ratios = deque(maxlen=3)
self.tops_ratios = deque(maxlen=3)
self.indexes_pairs = None
self.top_ratios_pairs = None
self.bottoms_ratios_pairs = None
self.xd_line = None
def __init__(self, algo, zigzag_sigma: float, err_thresh: float = 1.2) -> None:
"""
Description:
This module identifies if there currently is a harmonics pattern visible in a given set of data
Arguments:
*zigzag_sigma: a percentage change in price which must occur for the zigzag to record a top or a bottom
"""
self.algo = algo
self.zigzag_sigma = zigzag_sigma
self.indexes = deque(maxlen=5)
self.err_thresh = err_thresh
# Define Patterns
self.GARTLEY = self.XABCD_RATIOS(0.618, [0.382, 0.886], [1.13, 1.618], 0.786, "Gartley")
self.BAT = self.XABCD_RATIOS([0.382, 0.50], [0.382, 0.886], [1.618, 2.618], 0.886, "Bat")
self.BUTTERFLY = self.XABCD_RATIOS(0.786, [0.382, 0.886], [1.618, 2.24], [1.27, 1.41], "Butterfly")
self.CRAB = self.XABCD_RATIOS([0.382, 0.618], [0.382, 0.886], [2.618, 3.618], 1.618, "Crab")
self.DEEP_CRAB = self.XABCD_RATIOS(0.886, [0.382, 0.886], [2.0, 3.618], 1.618, "Deep Crab")
self.CYPHER = self.XABCD_RATIOS([0.382, 0.618], [1.13, 1.41], [1.27, 2.00], 0.786, "Cypher")
self.ALL_PATTERNS = [self.GARTLEY, self.BAT, self.BUTTERFLY, self.CRAB, self.DEEP_CRAB, self.CYPHER]
def zigzag(self, close: np.array, high: np.array, low: np.array):
"""
Description:
Function is a zigzag indicator which draws ups and downs of the market.
For each turning point to occur price must move at least the % amount of sigma
Arguments:
*close: np.array of close prices
*high: np.array of close prices
*low: np.array of close prices
Returns:
tops: a list of all identified tops in the given set of data
bottoms: a list of all identified tops in the given set of data
"""
up_zig = True # Last extreme is a bottom. Next is a top.
tmp_max = high[0]
tmp_min = low[0]
tmp_max_i = 0
tmp_min_i = 0
tops = []
bottoms = []
for i in range(len(close)):
if up_zig: # Last extreme is a bottom
if high[i] > tmp_max:
# New high, update
tmp_max = high[i]
tmp_max_i = i
elif close[i] < tmp_max - tmp_max * self.zigzag_sigma:
# Price retraced by sigma %. Top confirmed, record it
# top[0] = confirmation index
# top[1] = index of top
# top[2] = price of top
top = [i, tmp_max_i, tmp_max]
tops.append(top)
# Setup for next bottom
up_zig = False
tmp_min = low[i]
tmp_min_i = i
else: # Last extreme is a top
if low[i] < tmp_min:
# New low, update
tmp_min = low[i]
tmp_min_i = i
elif close[i] > tmp_min + tmp_min * self.zigzag_sigma:
# Price retraced by sigma %. Bottom confirmed, record it
# bottom[0] = confirmation index
# bottom[1] = index of bottom
# bottom[2] = price of bottom
bottom = [i, tmp_min_i, tmp_min]
bottoms.append(bottom)
# Setup for next top
up_zig = True
tmp_max = high[i]
tmp_max_i = i
return tops, bottoms
def get_error(self, required_ratio: Union[float, list], current_ratio: float) -> bool:
"""
Description:
Function checks the error between the required ratio, which is found in
pattern definitions and the current ratio, found in the market from one zigzag
point to another.
Arguments:
*required_ratio: Union[float, list] Is a ratio that is required for the given pattern
if required ratio is a list, the lenght must be equal to 2
*current_ratio: float Is a ratio that is currently observed in the market between given zigzag points
Returns:
The absolute difference between two ratios. If required ratio is a list, then returns the smaller error from them
"""
if isinstance(required_ratio, list):
# If ratio is a list where one of the values are None, then only compare the value thats not None
if required_ratio[0] is None:
return abs(required_ratio[0]-current_ratio)
elif required_ratio[1] is None:
return abs(required_ratio[1]-current_ratio)
else:
ValueError('Required Ratio in get_error function is all None')
# If current ratio falls between the bounds, the error is 0
if min([required_ratio[0], required_ratio[1]]) < current_ratio and current_ratio < max([required_ratio[0], required_ratio[1]]):
return 0
# Otherwise return the minimum error from the difference of the closest bound
else:
return min([abs(required_ratio[0]-current_ratio), abs(required_ratio[1]-current_ratio)])
# If required ratio for that data point is None, then there is no error, thus return 0
if required_ratio is None:
return 0
# In all other cases return the absolute difference between ratios
return abs(current_ratio-required_ratio)
def call_after_plotting(self, added_last_value):
if added_last_value:
self.indexes.pop()
added_last_value = False
def harmonics(self, subset: pd.DataFrame, return_tuples: bool = False):
XABCD_ = self.XABCD()
UTILS_ = self.UTILS()
close = subset['Close']
high = subset['High']
low = subset['Low']
ad = False
tops, bottoms = self.zigzag(np.array(close), np.array(high), np.array(low))
if len(tops) > 0 and len(bottoms) > 0 and len(subset) > 0:
# Index of the last top and the last bottom
tops_date = subset.iloc[tops[-1][1]].name
bottoms_date = subset.iloc[bottoms[-1][1]].name
# if this last top/bottom is currently not recorded- record it
self.indexes.append(tops_date) if tops_date not in self.indexes else None
self.indexes.append(bottoms_date) if bottoms_date not in self.indexes else None
# Also add the last bar
if (subset.iloc[-1].name) not in self.indexes:
self.indexes.append((subset.iloc[-1].name))
ad = True
# Show ratio (retracement) lines from peaks of zigzag
for l, top in enumerate(tops):
top_date = subset.iloc[tops[l][1]].name
if top_date not in UTILS_.tops_ratios:
if len(UTILS_.tops_ratios) > 0 and top_date > UTILS_.tops_ratios[-1]:
UTILS_.tops_ratios.append(top_date)
elif len(UTILS_.tops_ratios) == 0:
UTILS_.tops_ratios.append(top_date)
for l, bottom in enumerate(bottoms):
bottom_date = subset.iloc[bottoms[l][1]].name
if bottom_date not in UTILS_.bottoms_ratios:
if len(UTILS_.bottoms_ratios) > 0 and bottom_date > UTILS_.bottoms_ratios[-1]:
UTILS_.bottoms_ratios.append(bottom_date)
elif len(UTILS_.bottoms_ratios) == 0:
UTILS_.bottoms_ratios.append(bottom_date)
# Extent ratio line to the very last candle
if UTILS_.tops_ratios[-1] < UTILS_.bottoms_ratios[-1]:
if subset.iloc[-1].name not in UTILS_.tops_ratios:
UTILS_.tops_ratios.append(subset.iloc[-1].name)
UTILS_.bottoms_ratios.popleft()
else:
if subset.iloc[-1].name not in UTILS_.bottoms_ratios:
UTILS_.bottoms_ratios.append(subset.iloc[-1].name)
UTILS_.tops_ratios.popleft()
all_candles = len(subset)
# Get the index of each point (DateTime)
D_index = self.indexes[-1] if len(self.indexes) > 0 else None
C_index = self.indexes[-2] if len(self.indexes) > 1 else None
B_index = self.indexes[-3] if len(self.indexes) > 2 else None
A_index = self.indexes[-4] if len(self.indexes) > 3 else None
X_index = self.indexes[-5] if len(self.indexes) > 4 else None
XABCD_.D = subset.loc[D_index]
XABCD_.C = (subset.index.get_loc(C_index)+1) if (C_index and C_index in subset.index) else None
XABCD_.B = (subset.index.get_loc(B_index)+1) if (B_index and B_index in subset.index) else None
XABCD_.A = (subset.index.get_loc(A_index)+1) if (A_index and A_index in subset.index) else None
XABCD_.X = (subset.index.get_loc(X_index)+1) if (X_index and X_index in subset.index) else None
# Fetch its price
XABCD_.D_price = close[-1]
if XABCD_.C:
XABCD_.C_price = subset['Low' if (XABCD_.D_price > close[-int(all_candles-XABCD_.C+1)]) else 'High'][-int(all_candles-XABCD_.C+1)]
else:
XABCD_.C_price = None
if XABCD_.B and XABCD_.C:
XABCD_.B_price = subset['Low' if (XABCD_.D_price < close[-int(all_candles-XABCD_.C+1)]) else 'High'][-int(all_candles-XABCD_.B+1)]
else:
XABCD_.B_price = None
if XABCD_.A and XABCD_.C:
XABCD_.A_price = subset['Low' if (XABCD_.D_price > close[-int(all_candles-XABCD_.C+1)]) else 'High'][-int(all_candles-XABCD_.A+1)]
else:
None
if XABCD_.X and XABCD_.C:
XABCD_.X_price = subset['Low' if (XABCD_.D_price < close[-int(all_candles-XABCD_.C+1)]) else 'High'][-int(all_candles-XABCD_.X+1)]
else:
XABCD_.X_price = None
DC_h = abs(XABCD_.D_price - XABCD_.C_price) if (XABCD_.D_price and XABCD_.C_price) else None
CB_h = abs(XABCD_.C_price - XABCD_.B_price) if (XABCD_.C_price and XABCD_.B_price) else None
BA_h = abs(XABCD_.B_price - XABCD_.A_price) if (XABCD_.B_price and XABCD_.A_price) else None
AX_h = abs(XABCD_.A_price - XABCD_.X_price) if (XABCD_.A_price and XABCD_.X_price) else None
XABCD_.DB_ratio = (DC_h / CB_h) if CB_h else None
XABCD_.AC_ratio = (CB_h / BA_h) if BA_h else None
XABCD_.XB_ratio = (BA_h / AX_h) if AX_h else None
try:
XABCD_.XD_ratio = (AX_h / DC_h) if AX_h else None
except:
XABCD_.XD_ratio = 0
best_err = 1e30
best_pat = None
if XABCD_.DB_ratio and XABCD_.AC_ratio and XABCD_.XB_ratio and XABCD_.XD_ratio:
for pat in self.ALL_PATTERNS:
err = 0.0
err += self.get_error(pat.DB, XABCD_.DB_ratio)
err += self.get_error(pat.AC, XABCD_.AC_ratio)
err += self.get_error(pat.XB, XABCD_.XB_ratio)
err += self.get_error(pat.XD, XABCD_.XD_ratio)
if err < best_err:
best_err = err
best_pat = pat.name
if best_err <= self.err_thresh:
if XABCD_.D_price > XABCD_.C_price:
best_pat = f'Bearish {best_pat}'
else:
best_pat = f'Bullish {best_pat}'
else:
best_pat = None
if not return_tuples:
return UTILS_, XABCD_, best_pat, ad
else:
if len(self.indexes) > 1:
UTILS_.indexes_pairs = list(zip(self.indexes, itertools.islice(self.indexes, 1, None)))
UTILS_.top_ratios_pairs = list(zip(UTILS_.tops_ratios, itertools.islice(UTILS_.tops_ratios, 1, None)))
UTILS_.bottoms_ratios_pairs = list(zip(UTILS_.bottoms_ratios, itertools.islice(UTILS_.bottoms_ratios, 1, None)))
UTILS_.xd_line = list(zip([self.indexes[0]], [self.indexes[-1]]))
return UTILS_, XABCD_, best_pat, ad
else:
return UTILS_, XABCD_, best_pat, ad
if not return_tuples:
return UTILS_, XABCD_, None, ad
else:
return UTILS_, XABCD_, None, ad# ----< Global Imports >----
from AlgorithmImports import *
import numpy as np
from scipy.optimize import minimize, LinearConstraint
import pandas as pd
import timeit
class Trendline_Identification():
"""
Class draws support and resistance lines
Please call fit_trendlines() function which returns coefficients:
*** coefficient[0] = slope
*** coefficient[1] = intercept
Build a trendline with "y = slope * x + intercept" function
"""
def __init__(self, total_candles_visible: int):
self.pattern = None
self.total = total_candles_visible
def check_trend_line(self, support: bool, pivot: int, slope: float, y: np.array):
# Compute sum of differences between line and prices
# return negative val if invalid
# Find the intercept of the line going through pivot point with given slope
intercept = -slope * pivot + y[pivot]
line_vals = slope * np.arange(len(y)) + intercept
diffs = line_vals - y
# Check to see if the line is valid, return -1 if it is not valid
if support and diffs.max() > 1e-5:
return -1.0
elif not support and diffs.min() < -1e-5:
return -1.0
# Squared sum of diffs between data and line
err = (diffs ** 2.0).sum()
return err
def optimize_slope(self, support: bool, pivot: int, init_slope: float, y: np.array):
# Amount to change slope by. Multiplyed by opt_step
slope_unit = (y.max() - y.min()) / len(y)
# Optimization variables
opt_step = 1.0
min_step = 0.001
curr_step = opt_step
# Initiate at the slope of the line of best fit
best_slope = init_slope
best_err = self.check_trend_line(support, pivot, init_slope, y)
assert(best_err >= 0.0)
get_derivative = True
derivative = None
while curr_step > min_step:
if get_derivative:
# Numerical differentiation, increase slope by very small amount
# to see if error increases/decreases
# Gives us the direction to change slope
slope_change = best_slope + slope_unit * min_step
test_err = self.check_trend_line(support, pivot, slope_change, y)
derivative = test_err - best_err
# If increasing by a small amount fails,
# try decreasing by a small amount
if test_err < 0.0:
slope_change = best_slope - slope_unit * min_step
test_err = self.check_trend_line(support, pivot, slope_change, y)
derivative = best_err - test_err
if test_err < 0.0: # Derivative failed, give up
raise Exception("Derivative failed. Check your data")
get_derivative = False
if derivative > 0.0: # Increasing slope increased error
test_slope = best_slope - slope_unit * curr_step
else: # Increasing slope decreased error
test_slope = best_slope + slope_unit * curr_step
test_err = self.check_trend_line(support, pivot, test_slope, y)
if test_err < 0 or test_err >= best_err:
# slope failed/didn't reduce error
curr_step *= 0.5 # Reduce step size
else: # Test slope reduced error
best_err = test_err
best_slope = test_slope
get_derivative = True
# Optimize done, return best slope and intercept
return (best_slope, -best_slope * pivot + y[pivot])
def fit_trendlines(self, close: np.array, high: np.array = None, low: np.array = None):
X = np.arange(len(close))
best_fit_coefs = np.polyfit(X, close, 1)
line_points = best_fit_coefs[0] * X + best_fit_coefs[1]
upper_pivot = ((high if high is not None else close) - line_points).argmax()
lower_pivot = ((low if low is not None else close)- line_points).argmin()
# Optimize the slope for both trend lines
support_coefs = self.optimize_slope(True, lower_pivot, best_fit_coefs[0], (low if low is not None else close))
resist_coefs = self.optimize_slope(False, upper_pivot, best_fit_coefs[0], (high if high is not None else close))
# If intercept positive, then trendlines will intercept in the future. If negative, they have already inrecepted
intercept_x = ((support_coefs[1] - resist_coefs[1]) / (support_coefs[0] - resist_coefs[0])) - self.total + 1
if support_coefs[0] > 0 and resist_coefs[0] > 0:
# We know its a bullish range upwards
if intercept_x > 0 and intercept_x < self.total:
self.pattern = 'rising wedge'
if intercept_x < 0 and abs(intercept_x) < self.total:
self.pattern = 'expanding range'
else:
self.pattern = 'bullish channel'
if support_coefs[0] < 0 and resist_coefs[0] < 0:
# We know its a bearish range downwards
if intercept_x > 0 and intercept_x < self.total:
self.pattern = 'falling wedge'
if intercept_x < 0 and abs(intercept_x) < self.total:
self.pattern = 'expanding range'
else:
self.pattern = 'bearish channel'
if support_coefs[0] > 0 and resist_coefs[0] < 0:
if intercept_x > 0 and intercept_x < self.total:
self.pattern = 'triangle'
else:
self.pattern = 'range'
if support_coefs[0] < 0 and resist_coefs[0] > 0:
self.pattern = 'range'
return (self.pattern, best_fit_coefs, support_coefs, resist_coefs)