Plotly: How to calculate and illustrate the upper and lower 50% of a trend line?

I need to find (print) points of a plot that are 50% lower and upper of a trend line using Python and preferably using Plotly. I could make so far a trend line of a positive linear trend using plotly.

I want to get something like this graph that I found here:

enter image description here

Answer

Since you haven’t provided a dataset, I’m going to build a solution based on an earlier answer of mine: Plotly: How to add trendline and parallel lines to time series data?. And here’s the resulting plot for your use case:

enter image description here

I can’t guarantee that the dotted lines are 100% what you’re looking for, since it will be a matter of opinion how your desired lines should be calculated. What I’ve done here is simply finding which values lie above the trend line, take the average, and add that average to the constant term when calculating the accompanying top 50 % line in:

[(const+over50) + (trend*i) for i,v in enumerate(df.index)]

But using the average only makes real sense if the points are somewhat evenly distributed over the trend line. And as far as I know, you may prefer to count half of the values over the trend line, and make a line there. But I’ll leave that for you to decide. Anyway, if your end goal is to mark or highlight thosw points, the code snippet below will let you end up with this:

enter image description here

I hope you’ll find this suggestion useful. And please don’t hesitate to let me know if anything is unclear.

Complete code with sample data

import plotly.graph_objects as go
import statsmodels.api as sm
import pandas as pd
import numpy as np
import datetime
from pandas import Timestamp

df = pd.DataFrame({'Date': {3762: Timestamp('2001-06-01 00:00:00'),
  3763: Timestamp('2001-06-04 00:00:00'),
  3764: Timestamp('2001-06-05 00:00:00'),
  3765: Timestamp('2001-06-06 00:00:00'),
  3766: Timestamp('2001-06-07 00:00:00'),
  3767: Timestamp('2001-06-08 00:00:00'),
  3768: Timestamp('2001-06-11 00:00:00'),
  3769: Timestamp('2001-06-12 00:00:00'),
  3770: Timestamp('2001-06-13 00:00:00'),
  3771: Timestamp('2001-06-14 00:00:00'),
  3772: Timestamp('2001-06-15 00:00:00'),
  3773: Timestamp('2001-06-18 00:00:00'),
  3774: Timestamp('2001-06-19 00:00:00'),
  3775: Timestamp('2001-06-20 00:00:00'),
  3776: Timestamp('2001-06-21 00:00:00'),
  3777: Timestamp('2001-06-22 00:00:00'),
  3779: Timestamp('2001-06-26 00:00:00'),
  3780: Timestamp('2001-06-27 00:00:00'),
  3781: Timestamp('2001-06-28 00:00:00'),
  3782: Timestamp('2001-06-29 00:00:00'),
  3784: Timestamp('2001-07-03 00:00:00'),
  3785: Timestamp('2001-07-04 00:00:00'),
  3786: Timestamp('2001-07-05 00:00:00'),
  3788: Timestamp('2001-07-09 00:00:00'),
  3789: Timestamp('2001-07-10 00:00:00'),
  3790: Timestamp('2001-07-11 00:00:00'),
  3791: Timestamp('2001-07-12 00:00:00'),
  3792: Timestamp('2001-07-13 00:00:00'),
  3793: Timestamp('2001-07-16 00:00:00'),
  3794: Timestamp('2001-07-17 00:00:00'),
  3795: Timestamp('2001-07-18 00:00:00'),
  3796: Timestamp('2001-07-19 00:00:00'),
  3797: Timestamp('2001-07-20 00:00:00'),
  3798: Timestamp('2001-07-23 00:00:00'),
  3799: Timestamp('2001-07-24 00:00:00'),
  3801: Timestamp('2001-07-26 00:00:00'),
  3802: Timestamp('2001-07-27 00:00:00'),
  3803: Timestamp('2001-07-30 00:00:00'),
  3804: Timestamp('2001-07-31 00:00:00'),
  3805: Timestamp('2001-08-01 00:00:00'),
  3806: Timestamp('2001-08-02 00:00:00'),
  3807: Timestamp('2001-08-03 00:00:00'),
  3808: Timestamp('2001-08-06 00:00:00'),
  3809: Timestamp('2001-08-07 00:00:00'),
  3810: Timestamp('2001-08-08 00:00:00'),
  3811: Timestamp('2001-08-09 00:00:00'),
  3812: Timestamp('2001-08-10 00:00:00'),
  3813: Timestamp('2001-08-13 00:00:00'),
  3814: Timestamp('2001-08-14 00:00:00'),
  3815: Timestamp('2001-08-15 00:00:00'),
  3816: Timestamp('2001-08-16 00:00:00'),
  3817: Timestamp('2001-08-17 00:00:00'),
  3818: Timestamp('2001-08-20 00:00:00'),
  3819: Timestamp('2001-08-21 00:00:00'),
  3820: Timestamp('2001-08-22 00:00:00'),
  3821: Timestamp('2001-08-23 00:00:00'),
  3822: Timestamp('2001-08-24 00:00:00'),
  3823: Timestamp('2001-08-27 00:00:00'),
  3824: Timestamp('2001-08-28 00:00:00'),
  3825: Timestamp('2001-08-29 00:00:00'),
  3826: Timestamp('2001-08-30 00:00:00'),
  3827: Timestamp('2001-08-31 00:00:00'),
  3828: Timestamp('2001-09-03 00:00:00'),
  3829: Timestamp('2001-09-04 00:00:00'),
  3830: Timestamp('2001-09-05 00:00:00'),
  3831: Timestamp('2001-09-06 00:00:00'),
  3832: Timestamp('2001-09-07 00:00:00'),
  3833: Timestamp('2001-09-10 00:00:00'),
  3834: Timestamp('2001-09-11 00:00:00'),
  3835: Timestamp('2001-09-12 00:00:00'),
  3836: Timestamp('2001-09-13 00:00:00'),
  3837: Timestamp('2001-09-14 00:00:00'),
  3838: Timestamp('2001-09-17 00:00:00'),
  3839: Timestamp('2001-09-18 00:00:00'),
  3840: Timestamp('2001-09-19 00:00:00'),
  3841: Timestamp('2001-09-20 00:00:00'),
  3842: Timestamp('2001-09-21 00:00:00'),
  3843: Timestamp('2001-09-24 00:00:00'),
  3844: Timestamp('2001-09-25 00:00:00'),
  3845: Timestamp('2001-09-26 00:00:00'),
  3846: Timestamp('2001-09-27 00:00:00'),
  3847: Timestamp('2001-09-28 00:00:00'),
  3850: Timestamp('2001-10-03 00:00:00'),
  3851: Timestamp('2001-10-04 00:00:00'),
  3852: Timestamp('2001-10-05 00:00:00'),
  3853: Timestamp('2001-10-08 00:00:00'),
  3854: Timestamp('2001-10-09 00:00:00'),
  3855: Timestamp('2001-10-10 00:00:00'),
  3856: Timestamp('2001-10-11 00:00:00'),
  3857: Timestamp('2001-10-12 00:00:00'),
  3858: Timestamp('2001-10-15 00:00:00'),
  3859: Timestamp('2001-10-16 00:00:00'),
  3860: Timestamp('2001-10-17 00:00:00'),
  3861: Timestamp('2001-10-18 00:00:00'),
  3862: Timestamp('2001-10-19 00:00:00'),
  3863: Timestamp('2001-10-22 00:00:00'),
  3864: Timestamp('2001-10-23 00:00:00'),
  3865: Timestamp('2001-10-24 00:00:00'),
  3866: Timestamp('2001-10-25 00:00:00'),
  3867: Timestamp('2001-10-26 00:00:00')},
 'Adj Close': {3762: 9.483521300451965,
  3763: 9.488539389609842,
  3764: 9.506873417520655,
  3765: 9.516059526271494,
  3766: 9.52540142267562,
  3767: 9.533067841143405,
  3768: 9.523360475569014,
  3769: 9.512419287352929,
  3770: 9.512170110321078,
  3771: 9.491669027751996,
  3772: 9.480558330676322,
  3773: 9.468756875278643,
  3774: 9.48293369128291,
  3775: 9.466431924131614,
  3776: 9.487020913528825,
  3777: 9.486001951740908,
  3779: 9.469774943465724,
  3780: 9.473028427171643,
  3781: 9.459371553309266,
  3782: 9.475970855997938,
  3784: 9.486816137667164,
  3785: 9.488542421142602,
  3786: 9.472664671722018,
  3788: 9.448623120188204,
  3789: 9.450451192873874,
  3790: 9.435713467289014,
  3791: 9.446218508764293,
  3792: 9.442466660552066,
  3793: 9.443397047352386,
  3794: 9.433103851072097,
  3795: 9.427642127580112,
  3796: 9.41571256910222,
  3797: 9.417491092037041,
  3798: 9.412174497254961,
  3799: 9.4103462690634,
  3801: 9.39597479458201,
  3802: 9.407728679911855,
  3803: 9.399857656975392,
  3804: 9.418710567070383,
  3805: 9.431781694039891,
  3806: 9.430789907045172,
  3807: 9.414837561626188,
  3808: 9.404986466190781,
  3809: 9.39326095182,
  3810: 9.389156606132271,
  3811: 9.368776387849374,
  3812: 9.372953110523751,
  3813: 9.366855970805329,
  3814: 9.391912461823267,
  3815: 9.404395312850555,
  3816: 9.378600227328686,
  3817: 9.37201776092802,
  3818: 9.34650456280641,
  3819: 9.344901824694107,
  3820: 9.32264802844274,
  3821: 9.33656588127212,
  3822: 9.315627867418097,
  3823: 9.326764237890817,
  3824: 9.332604930413563,
  3825: 9.327448527151956,
  3826: 9.333940224481115,
  3827: 9.313842403932533,
  3828: 9.29676020844021,
  3829: 9.318015638210596,
  3830: 9.300468022736998,
  3831: 9.27465889826041,
  3832: 9.248040717937537,
  3833: 9.246317398619535,
  3834: 9.25122895807117,
  3835: 9.158375285355174,
  3836: 9.166305927329747,
  3837: 9.175277821947487,
  3838: 9.13984812080253,
  3839: 9.1386188229253,
  3840: 9.165149513582218,
  3841: 9.139701196323891,
  3842: 9.097641909876808,
  3843: 9.13610162204065,
  3844: 9.128051597198034,
  3845: 9.145455124069166,
  3846: 9.169600669798987,
  3847: 9.205398199033475,
  3850: 9.200001069931528,
  3851: 9.238576907009563,
  3852: 9.237700631328401,
  3853: 9.207118194132338,
  3854: 9.245604198507314,
  3855: 9.23972830855306,
  3856: 9.26128158783136,
  3857: 9.237384352858927,
  3858: 9.223314822990815,
  3859: 9.225080227987517,
  3860: 9.236087021069979,
  3861: 9.198329565352042,
  3862: 9.192770913389573,
  3863: 9.189886616720194,
  3864: 9.23208619279342,
  3865: 9.23439472833901,
  3866: 9.23439472833901,
  3867: 9.250016773018734},
 'Volume': {3762: 0.0,
  3763: 0.0,
  3764: 0.0,
  3765: 0.0,
  3766: 0.0,
  3767: 0.0,
  3768: 0.0,
  3769: 0.0,
  3770: 0.0,
  3771: 0.0,
  3772: 0.0,
  3773: 0.0,
  3774: 0.0,
  3775: 0.0,
  3776: 0.0,
  3777: 0.0,
  3779: 0.0,
  3780: 0.0,
  3781: 0.0,
  3782: 0.0,
  3784: 0.0,
  3785: 0.0,
  3786: 0.0,
  3788: 257038800.0,
  3789: 134407800.0,
  3790: 195057600.0,
  3791: 174767800.0,
  3792: 211230200.0,
  3793: 113928800.0,
  3794: 139890800.0,
  3795: 134535000.0,
  3796: 204987000.0,
  3797: 147662000.0,
  3798: 166057200.0,
  3799: 139913800.0,
  3801: 221039000.0,
  3802: 124388600.0,
  3803: 153086200.0,
  3804: 227109800.0,
  3805: 243126000.0,
  3806: 194471600.0,
  3807: 168728800.0,
  3808: 141753200.0,
  3809: 208445200.0,
  3810: 178200800.0,
  3811: 231948800.0,
  3812: 148634200.0,
  3813: 137231600.0,
  3814: 172713800.0,
  3815: 191067400.0,
  3816: 422805600.0,
  3817: 330698600.0,
  3818: 256960200.0,
  3819: 225189800.0,
  3820: 272482800.0,
  3821: 215469200.0,
  3822: 241046000.0,
  3823: 145020400.0,
  3824: 179275400.0,
  3825: 188285800.0,
  3826: 246490800.0,
  3827: 265702000.0,
  3828: 185143200.0,
  3829: 303746000.0,
  3830: 206642600.0,
  3831: 239079600.0,
  3832: 399700800.0,
  3833: 367156400.0,
  3834: 0.0,
  3835: 0.0,
  3836: 0.0,
  3837: 0.0,
  3838: 0.0,
  3839: 0.0,
  3840: 333256200.0,
  3841: 284966400.0,
  3842: 519940400.0,
  3843: 442181500.0,
  3844: 367545800.0,
  3845: 390860600.0,
  3846: 296667600.0,
  3847: 320775600.0,
  3850: 333197400.0,
  3851: 358779000.0,
  3852: 576213400.0,
  3853: 511535600.0,
  3854: 409534200.0,
  3855: 370696800.0,
  3856: 398527200.0,
  3857: 388528000.0,
  3858: 275161200.0,
  3859: 192816600.0,
  3860: 414838800.0,
  3861: 365696000.0,
  3862: 297211400.0,
  3863: 236566000.0,
  3864: 344018800.0,
  3865: 287418600.0,
  3866: 0.0,
  3867: 346798600.0}})

df = df.tail(25)
# line parameters using statsmodels
df['Date'] = pd.to_datetime(df['Date'])
df['ix']=np.arange(0, len(df))


mod = sm.OLS(df['Adj Close'],sm.add_constant(df.ix)).fit()
const = mod.params[0]
trend = mod.params[1]

# dict that stores adjusted constants (starting points)
extra_lines = [-0.2,-0.1,0, 0.1,0.2] # add or  remove as you please
model = [{'Line': 'Line_'+str(i+1), 'value': k, 'const': const+k} for i, k in enumerate(extra_lines)]
df['trend'] = [const + (trend*i) for i,v in enumerate(df.index)]

# make more sensible names
df.columns = ['date', 'value', 'volume', 'ix', 'trend']

# calculate distance from trend lines to 50% over and under the trend line
df['over'] = np.where(df['value']>df['trend'], df['value']-df['trend'], np.nan)
df['under'] = np.where(df['value']<df['trend'], df['trend']-df['value'], np.nan)
over50 = np.mean(df['over'])
under50 = np.mean(df['under'])
df['over50_line'] = [(const+over50) + (trend*i) for i,v in enumerate(df.index)]
df['under50_line'] = [(const-under50) + (trend*i) for i,v in enumerate(df.index)]
df['top50'] = np.where(df['value']>df['trend'], df['value'], np.nan)
df['bottom50'] = np.where(df['value']<df['trend'], df['value'], np.nan)

# plotly figure with sample data
fig = go.Figure(go.Scatter(x=df.ix, y=df['value'],
                           mode = 'markers',
                           marker = dict(color='rgba(0,0,255,0.2)',
                                         line=dict(color='blue', width=2),
                                         symbol = 'diamond',
                                         size = 12),
                           name = 'values'))
# add trend line
fig.add_trace(go.Scatter(x=df.ix, y = df['trend'],
                         name = 'trend'))

# add line for 50% over the trend line
fig.add_trace(go.Scatter(x=df.ix, y = df['over50_line'],
                         mode = 'lines', 
                         line = dict(dash='dash', color = 'red', width = 1),
                         name = 'top 50 %'))

# add line for 50% under the trend line
fig.add_trace(go.Scatter(x=df.ix, y =df['under50_line'],
                         mode = 'lines', 
                         line = dict(dash='dash', color = 'red', width = 1),
                         name = 'lower 50%'))

# add markers for top25 / over 50% line
fig.add_trace(go.Scatter(x=df.ix, y=df['top50'],
                           mode = 'markers',
                           marker = dict(color='yellow',
                                         #line=dict(color='blue', width=2),
                                         symbol = 'circle',
                                         size = 5),
                           name = 'over50%'))

# add markers for bottom25 / under 50% line
fig.add_trace(go.Scatter(x=df.ix, y=df['bottom50'],
                           mode = 'markers',
                           marker = dict(color='red',
                                         #line=dict(color='blue', width=2),
                                         symbol = 'circle',
                                         size = 5),
                           name = 'under 50%'))

   
fig.show()