Public
Edited
Sep 26, 2023
Importers
Insert cell
Insert cell
function addDays(d, days) {
let x = new Date(d);
x.setDate(x.getDate() + days);
return x;
}
Insert cell
function addMonths(d, months) {
let x = new Date(d);
x.setMonth(x.getMonth() + months);
return x;
}
Insert cell
function addYears(d, years) {
let x = new Date(d);
x.setFullYear(x.getFullYear() + years);
return x;
}
Insert cell
sp500 = FileAttachment("sp500_history@1.csv").csv({typed: true})
Insert cell
sp500[0]
Insert cell
dates = d3.map(sp500, d => d.Date)
Insert cell
target = addYears(sp500[2]['Date'], 1)
Insert cell
targetPos = d3.bisectLeft(dates, target)
Insert cell
[dates[targetPos-1], dates[targetPos], dates[targetPos+1]]
Insert cell
d3.max(sp500, d => d['Close'])
Insert cell
// https://observablehq.com/@d3/d3-bisect
function seekDates(dates, d) {
let i = d3.bisectLeft(dates, d);
if (i > 0 && dates[i] > d) {
i = i - 1;
} else if (i >= dates.length) {
i = dates.length - 1;
}
return i;
}
Insert cell
seekDates(dates, target)
Insert cell
sliced = sp500.slice(0, 253+1)
Insert cell
sliced[sliced.length-1]
Insert cell
d3.min(sliced, d => d.Close)
Insert cell
function getLast(arr) {
return arr[arr.length-1];
}
Insert cell
function formatDays(days) {
return (days > 1) ? `${days} days` : `${days} day`
}
Insert cell
// https://github.com/d3/d3-time

function getElaspedTime(fromDate, toDate) {
return formatDays(d3.timeDay.count(fromDate, toDate))
}
Insert cell
function getChangeInPercentage(fromPrice, toPrice) {
return (toPrice - fromPrice) * 100 / fromPrice;
}
Insert cell
function computeRecessionCandidates(data, dates, dateColumn, priceColumn, recessionRate) {
const L = data.length;
let relativeLows = [0];
let curPos = 1;
let records = [];
while (curPos < L) {
const curDate = data[curPos][dateColumn];
const curPrice = data[curPos][priceColumn];
const endPos = seekDates(dates, addYears(curDate, 1));
const sliced = data.slice(curPos, endPos+1);
if (d3.min(sliced, d => d[priceColumn]) >= curPrice) {
const lastLowPos = getLast(relativeLows);
const maxPosOffset = d3.maxIndex(data.slice(lastLowPos, curPos), d => d[priceColumn]);
const maxPos = lastLowPos + maxPosOffset;
const maxPrice = data[maxPos][priceColumn];
const discount = 1 - recessionRate
if (maxPrice * discount > curPrice) {
const preHighDate = data[maxPos][dateColumn];
const preHighClose = data[maxPos][priceColumn];
records.push({
preHighDate: preHighDate,
preHighClose: preHighClose,
lowDate: curDate,
lowClose: curPrice,
change: getChangeInPercentage(preHighClose, curPrice),
duration: getElaspedTime(preHighDate, curDate)
});
relativeLows.push(curPos);
}
curPos += 1;
} else {
let minPosOffset = d3.minIndex(data.slice(curPos, endPos), d => d[priceColumn]);
if (minPosOffset == 0) {
curPos += 1;
} else {
let minPos = curPos + minPosOffset;
curPos = minPos;
}
}
}

return records;
}
Insert cell
function updateLowUntilClose(records, data, dates, dateColumn, priceColumn, recoverRate = recoverRate) {
const invalidMax = d3.max(data, d => d[priceColumn]);
for (let record of records) {
const lowUtilCloseEstimate = (record['preHighClose'] - record['lowClose']) * recoverRate + record['lowClose'];
const lowPos = seekDates(dates, record['lowDate']);
const sliced = data.slice(lowPos, data.length)
const lowUtilOffset = d3.minIndex(sliced, d => ((d[priceColumn] > lowUtilCloseEstimate) ? 1 : null))
const lowUtilPos = lowPos + lowUtilOffset;
record['lowUtilCloseEstimate'] = lowUtilCloseEstimate;
record['lowUtilClose'] = data[lowUtilPos][priceColumn];
record['lowUtilDate'] = data[lowUtilPos][dateColumn];
record['lowUtilDuration'] = getElaspedTime(data[lowPos][dateColumn], data[lowUtilPos][dateColumn]);
record['lowUtilChange'] = getChangeInPercentage(data[lowPos][priceColumn], data[lowUtilPos][priceColumn]);
}
}
Insert cell
function filterOverlappingPreLowUntilToCurPreHigh(records, dateColumn, priceColumn) {
let prePos = 0;
const L = records.length;
let A = Array(L).fill(true);
for (let i = 1; i < L; ++i) {
if (records[i]['preHighDate'] <= records[prePos]['lowUtilDate']) {
A[i] = false;
} else {
prePos = i;
}
}
return d3.filter(records, (d, i) => A[i]);
}
Insert cell
function updatePreHighForConsolidation(records, data, dates, dateColumn, priceColumn, consolidationRate=0.2) {
// https://stackoverflow.com/questions/34348937/access-to-es6-array-element-index-inside-for-of-loop
// for (let record of records) {
for (let i = 0; i < records.length; ++i) {
const preHigh = records[i]['preHighClose'];
const preHighPos = seekDates(dates, records[i]['preHighDate']);
const low = records[i]['lowClose'];
const lowPos = seekDates(dates, records[i]['lowDate']);
records[i]['preHighPos'] = preHighPos;
records[i]['lowPos'] = lowPos;
const preHighEstimated = low + (preHigh - low) * (1 - consolidationRate);
records[i]['preHighEstimated'] = preHighEstimated;
const sliced = data.slice(preHighPos, lowPos+1);
let newPreHighPosOffset = 0;
for (let k = sliced.length-1; k >= 0; --k) {
if (sliced[k][priceColumn] > preHighEstimated) {
break;
}
newPreHighPosOffset = k;
}
records[i]['newPreHighPosOffset'] = newPreHighPosOffset;
const newPreHighPos = preHighPos + newPreHighPosOffset;
if (newPreHighPos !== preHighPos) {
records[i]['preHighDate'] = data[newPreHighPos][dateColumn];
records[i]['preHighClose'] = data[newPreHighPos][priceColumn];
records[i]['Cool'] = 'Cool';
}
}
}
Insert cell
function computeRecessions(data, dateColumn='Date', priceColumn='Close', recessionRate = 0.1, recoverRate = 0.5, consolidationRate = 0.2) {
const dates = d3.map(data, d => d[dateColumn]);
let result = computeRecessionCandidates(data, dates, dateColumn=dateColumn, priceColumn=priceColumn, recessionRate=recessionRate);
updateLowUntilClose(result, data, dates, dateColumn=dateColumn, priceColumn=priceColumn, recoverRate=recoverRate);
result = filterOverlappingPreLowUntilToCurPreHigh(result, dateColumn=dateColumn, priceColumn=priceColumn);
updatePreHighForConsolidation(result, data, dates, dateColumn=dateColumn, priceColumn=priceColumn, consolidationRate=consolidationRate);
return result;
}
Insert cell
result = computeRecessions(sp500)
Insert cell
result[0]
Insert cell
Insert cell
// https://en.wikipedia.org/wiki/List_of_recessions_in_the_United_States
us_recessions_wiki = FileAttachment("us_recessions_wiki.csv").csv({typed: true})
Insert cell
// https://fred.stlouisfed.org/series/JHDUSRGDPBR
// Slight difference between two data sources
use_recessions_fred = {
let data = await FileAttachment("JHDUSRGDPBR.csv").csv({typed: true});
for (let i = 0; i < data.length; ++i) {
if (data[i].JHDUSRGDPBR == 0) {
data[i].FROM = null;
} else {
if (i > 0 && data[i-1].FROM !== null) {
data[i].FROM = data[i-1].FROM
} else {
data[i].FROM = data[i].DATE;
}
}
data[i].last = (data[i].JHDUSRGDPBR == 1) && (i == data.length - 1 || data[i+1].JHDUSRGDPBR == 0);
}
return d3.map(d3.filter(data, d => d.last), ({FROM, DATE}) => ({FROM, TO: DATE}))
}
Insert cell
// Imported in other notebooks
// us_recessions = us_recessions_wiki
us_recessions = use_recessions_fred
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more