Hi Shile,
I've been working on implementing the author's charting system they call "Intrinsic Time". Ideally this would be a tool which could employed in any strategy, and that's the objective I'm working towards; reusability. I don't think it's there yet, but here's a starting point. Included are some charts I was able to generate in a research book. Code provided below.
SPY OHLC:

SPY Intrinsic Time (using 3% change delta):

Â
I also have a C# implementation, which employs RollingWindow. I wasn't able to use RollingWindow in Python because it won't accept a Python class as the type parameter (is it possible to use a wildcard type, like the `any` type in TypeScript?).
The other challenges I ran into is that I don't see an obvious way to plot the data in a C# research book, and I don't know to wrap and then import my C# classes in a Python research book, which would allow me to implement this entirely in C#, and then test it in a Python research book, plot the results, and then use the C# code directly in a C# algo.
Research Book
qb = QuantBook()
spy = qb.AddEquity("SPY")
days = 360
history = qb.History(qb.Securities.Keys, days, Resolution.Daily)
import pandas as pd
import mplfinance as mpf
from IntrinsicTime import IntrinsicTimeChart
it = IntrinsicTimeChart(0.03, days + 1)
# write every new bar into table struct at IT run-time
new_table = {"Date":[], "Open": [], "High": [], "Low": [], "Close": []}
def writeBarToTable(bar):
new_table["Date"].append(bar.time)
new_table["Low"].append(bar.low)
new_table["High"].append(bar.high)
if bar.direction == 'u':
new_table["Open"].append(bar.low)
new_table["Close"].append(bar.high)
else:
new_table["Open"].append(bar.high)
new_table["Close"].append(bar.low)
it.OnStep = writeBarToTable
# push price history into IT
for index, row in history.iterrows():
bar = TradeBar()
bar.Open = row['open']
bar.High = row['high']
bar.Low = row['low']
bar.Close = row['close']
it.NewCandlestick(index[1], bar)
# plot intrinsic time series
df = pd.DataFrame(new_table)
df.index = df["Date"]
mpf.plot(df, type='candlestick')
# plot historical price as OHLC
spy = history.loc['SPY']
spy.rename(columns={"open": "Open", "high": "High", "low": "Low", "close": "Close", "volume": "Volume"}, inplace=True)
mpf.plot(spy)
Â
Intrinsic Time Python Implementation
Note: With full tick data, use the NewPrice method (this is the ideal approach). For OHLC data (which is suboptimal due to loss of resolution), use NewCandlestick. However, note that this method makes a compromise in assuming a particular order for processing the OHLC values per bar, and this may effect your results. If you use 1 second bars, and your percentage change is large enough, I think you won't be impacted by this compromise.
Note: This implementation uses recursion to produce charts without gaps, this is what I understood from the paper as being the intended approach. IOW: Any gaps in real price movement will be filled with bars in the intrinsic time series.
from QuantConnect.Data.Market import TradeBar
class IntrinsicTimeBar:
reversal: False
direction: None
time: None
high: None
low: None
def __init__(self, time, high: float, low: float, direction=None, reversal=False):
self.reversal = reversal
self.direction = direction
self.time = time
self.high = high
self.low = low
class IntrinsicTimeChart:
maxPercentageChange = None
maxLength = None
lengthNow = 0
OnStep = None
upper = None
lower = None
def __init__(self, max_percentage_change: float, length = 2):
self.maxPercentageChange = max_percentage_change
self.maxLength = max(2, length)
self.window = []
self.upper = None
self.lower = None
def __AddBar(self, new_price: float, new_bar: IntrinsicTimeBar):
self.window.insert(0, new_bar)
length = len(self.window)
if length > self.maxLength:
self.window.pop(self.maxLength)
else:
self.lengthNow = length
self.upper = new_price
self.lower = new_price
if self.OnStep:
self.OnStep(new_bar)
def NewCandlestick(self, time, bar: TradeBar):
if self.upper == None:
self.upper = bar.Open
self.lower = bar.Open
avg_level = (self.upper + self.lower) * 0.5
ohlc_order = [
('Open', abs(avg_level - bar.Open)),
('High', abs(avg_level - bar.High)),
('Low', abs(avg_level - bar.Close)),
('Close', abs(avg_level - bar.Close))
]
ohlc_order = [t[0] for t in sorted(ohlc_order, key=lambda t: t[1])]
for prop in ohlc_order:
self.NewPrice(time, getattr(bar, prop))
def NewPrice(self, time, price: float):
self.upper = price if self.upper == None else max(price, self.upper)
self.lower = price if self.lower == None else min(price, self.lower)
allowable_change = (self.upper + self.lower) * 0.5 * self.maxPercentageChange
if self.upper - self.lower < allowable_change:
return
new_level = None
new_bar = IntrinsicTimeBar(time, self.upper, self.lower, direction=None, reversal=False)
possible_high = self.lower + allowable_change
possible_low = self.upper - allowable_change
if price > possible_high:
new_bar.high = new_level = possible_high
new_bar.direction = 'u'
if self.lengthNow > 0 and self.window[0].direction == 'd':
new_bar.reversal = True
elif price < possible_low:
new_bar.low = new_level = possible_low
new_bar.direction = 'd'
if self.lengthNow > 0 and self.window[0].direction == 'u':
new_bar.reversal = True
else:
raise Exception('the impossible has happened')
self.__AddBar(new_level, new_bar)
self.NewPrice(time, price)
Â
Just briefly, some assorted thoughts on programming languages on the platform:
I'm completely new to C#, and I'm really not a fan of OOP languages in general. However, the strong type checking in C# makes it much nicer to work with than Python. Sure would be nice if F# would become usable. I'm not really sure why it's an option on the platform, since selecting F# triggers an error saying "invalid language selection". I asked support and they replied that it's marked as beta, but I don't see beta mentioned anywhere in the interface.
Â