Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic regression trend tool overlay #114

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ package-lock.json
temp
/docs/docs/.vitepress/dist
dist
.vscode
6 changes: 4 additions & 2 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import indexBased from '../tests/tfs-test/allIndexBased.js'
import indicators from '../tests/indicators/indicators.js'
import rangeTool from '../tests/tools/rangeTool.js'
import lineTool from '../tests/tools/lineTool.js'
import regressionLineTool from '../tests/tools/regressionLineTool.js'
import watchPropTest from '../tests/navy/watchPropTest.js'

// More tests
Expand Down Expand Up @@ -89,8 +90,9 @@ onMount(() => {

stack.setGroup('tools-test')

//rangeTool(stack, chart)
lineTool(stack, chart)
// rangeTool(stack, chart)
// lineTool(stack, chart)
regressionLineTool(stack, chart)

stack.setGroup('navy-test')

Expand Down
8 changes: 8 additions & 0 deletions src/core/dataView.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ export default class DataView$ {
)
}

// As above, without range expansion.
makeUnexpandedSubset() {
return this.src.slice(
this.i1 + 1,
this.i2
)
}

}
3 changes: 2 additions & 1 deletion src/core/navy/overlayEnv.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import avgVolume from '../primitives/navyLib/avgVolume.js'
import roundRect from '../primitives/navyLib/roundRect.js'
import drawArrow from '../primitives/navyLib/arrow.js'
import TrendLine from '../primitives/navyLib/trendLine.js'
import RegressionTrend from '../primitives/navyLib/regressionTrend.js'
import Segment from '../primitives/navyLib/seg.js'
import Pin from '../primitives/navyLib/pin.js'
import {
Expand Down Expand Up @@ -59,7 +60,7 @@ export default class OverlayEnv {
candleBody, candleWick, volumeBar,
fastSma, avgVolume, candleColor,
roundRect, rescaleFont, drawArrow,
TrendLine, Segment, Pin,
TrendLine, Segment, Pin, RegressionTrend,
Utils
}

Expand Down
243 changes: 243 additions & 0 deletions src/core/primitives/navyLib/regressionTrend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// Interactive Regression Trend
// Combining line primitives, pins, and a linear regression

import { regressionMedLine } from '../../../stuff/linreg.js'

export default class RegressionTrend {
constructor(core, inputLine, nw = false, params = { extend: true }) {
this.extend = params.extend
this.core = core
this.data = inputLine
this.hover = false
this.selected = false
this.onSelect = () => {}
switch (inputLine.type) {
case 'segment':
this.inputLine = new core.lib.Segment(core)
break
}
this.pins = [
new core.lib.Pin(core, this, 'p1'),
new core.lib.Pin(core, this, 'p2')
]

for (var pin of this.pins) {
pin.onSettled = () => {
this.calculateRegression()
}
}
if (nw) {
this.pins[1].state = 'tracking'
}

this.boundaryPoints = {}
this.boundaryRays = [
new core.lib.Segment(core),
new core.lib.Segment(core)
]

if (this.extend) {
this.medianExtensionLine = new core.lib.Segment(core)
}
}

draw(ctx) {
// adjust input line
this.inputLine.update(this.data.p1, this.data.p2)

if (
!(
this.pins[1].state === 'dragging' ||
this.pins[0].state === 'dragging'
) &&
Object.keys(this.boundaryPoints).length != 0
) {
ctx.setLineDash([])
// ctx.lineWidth = 1.5
ctx.strokeStyle = '#5482f6'

// draw upper boundary ray
this.boundaryRays[0].update(
this.boundaryPoints.u1,
this.boundaryPoints.u2
)
ctx.beginPath()
this.boundaryRays[0].draw(ctx)
ctx.stroke()

// draw lower boundary ray
this.boundaryRays[1].update(
this.boundaryPoints.l1,
this.boundaryPoints.l2
)
ctx.beginPath()
this.boundaryRays[1].draw(ctx)
ctx.stroke()

// if extended, draw additional segment for median line
// begins at the endpoint of the inputline, ends halfway between the two boundary points.

if (this.extend) {
// very janky way of calculating p2
// need to reconsider output of regressionMedLine()

let median$2 =
(this.boundaryPoints.u2[1] + this.boundaryPoints.l2[1]) / 2
this.medianExtensionLine.update(this.data.p2, [
this.boundaryPoints.l2[0],
median$2
])
ctx.setLineDash([3, 3])
ctx.strokeStyle = '#b24d65'
ctx.beginPath()
this.medianExtensionLine.draw(ctx)
ctx.stroke()
ctx.setLineDash([])
}

// fill upper area

ctx.globalAlpha = 0.5
ctx.beginPath()
ctx.moveTo(this.boundaryRays[0].x1, this.boundaryRays[0].y1)
ctx.lineTo(this.boundaryRays[0].x2, this.boundaryRays[0].y2)
this.extend
? ctx.lineTo(
this.medianExtensionLine.x2,
this.medianExtensionLine.y2
)
: ctx.lineTo(this.inputLine.x2, this.inputLine.y2)
ctx.lineTo(this.inputLine.x1, this.inputLine.y1)
ctx.closePath()
ctx.fillStyle = '#1d2e5c'
ctx.fill()

// fill lower area
ctx.beginPath()
ctx.moveTo(this.boundaryRays[1].x1, this.boundaryRays[1].y1)
ctx.lineTo(this.boundaryRays[1].x2, this.boundaryRays[1].y2)
this.extend
? ctx.lineTo(
this.medianExtensionLine.x2,
this.medianExtensionLine.y2
)
: ctx.lineTo(this.inputLine.x2, this.inputLine.y2)
ctx.lineTo(this.inputLine.x1, this.inputLine.y1)
ctx.closePath()
ctx.fillStyle = '#5b2228'
ctx.fill()

ctx.globalAlpha = 1
}

ctx.setLineDash([3, 3])
// ctx.lineWidth = 1
this.pins[1].state === 'dragging' || this.pins[0].state === 'dragging'
? (ctx.strokeStyle = '#33ff33')
: (ctx.strokeStyle = '#b24d65')

ctx.beginPath()
this.inputLine.draw(ctx)
ctx.stroke()
ctx.setLineDash([])

// TODO: consider other conditions or mechanics for selection.
// want to have pins show whenever any of the RegressionTrend object is selected.
if (this.hover || this.selected) {
// console.log(this.pins[0].state, this.pins[1].state)
for (var pin of this.pins) {
pin.draw(ctx)
}
}
}

collision() {
const mouse = this.core.mouse
let [x, y] = [mouse.x, mouse.y]
return this.inputLine.collision(x, y)
}

propagate(name, data) {
for (var pin of this.pins) {
pin[name](data)
}
}

mousedown(event) {
this.propagate('mousedown', event)
if (this.collision()) {
this.onSelect(this.data.uuid)
}
}

mouseup(event) {
this.propagate('mouseup', event)
}

mousemove(event) {
this.hover = this.collision()
this.propagate('mousemove', event)
}

// there has to be an inbuilt for this, possibly within symbol.js?
// should be replaced with a param to choose source
transformCloseArray(inputArray) {
return inputArray.map((subArray) => subArray[4])
}

calculateRegression() {
console.log('finished tracking')
// filter dataSubset
let regressionWindowData = this.core.hub
.filter(this.core.hub.mainOv.dataSubset, [
this.pins[0].t,
this.pins[1].t
])
.makeUnexpandedSubset()

let closePoints = this.transformCloseArray(regressionWindowData)
// must be mindful that the output of regressionMedLine is using an index based approach, which is essentially normalizing the time values.
// TODO: refactor regressionMedLine to accomodate a third point $3 for extending the trendline
// Consider a function that takes a dataSubset and the two pin points as inputs
let { m, b, stdDev } = regressionMedLine(closePoints)
let $1 = m * 0 + b // for the first point
let $2 = m * (regressionWindowData.length - 1) + b // for the last point

this.pins[0].y$ = $1
this.pins[1].y$ = $2

// adjust input trend line points
this.data.p1 = [this.pins[0].t, $1]
this.data.p2 = [this.pins[1].t, $2]

// adjust upper boundary ray points
this.boundaryPoints.u1 = [this.pins[0].t, $1 + stdDev]
this.boundaryPoints.u2 = [this.pins[1].t, $2 + stdDev]

// adjust lower boundary ray points
this.boundaryPoints.l1 = [this.pins[0].t, $1 - stdDev]
this.boundaryPoints.l2 = [this.pins[1].t, $2 - stdDev]

// if extending, use the slope of the regression window but use the
// end point of our boundaries for the last value in dataSubset
// has to go from the first point of the regression, to the last point in the dataSubset.
if (this.extend) {
let tLast =
this.core.hub.mainOv.dataSubset[
this.core.hub.mainOv.dataSubset.length - 1
][0]

// there must be a way to do this without recalculating the datawindow.
// had to do this because the slope m is index based. Couldn't figure it out using just dataSubset.length and regressionWindowData.length
let extendedWindowData = this.core.hub
.filter(this.core.hub.mainOv.dataSubset, [
this.pins[0].t,
tLast
])
.makeUnexpandedSubset()
$2 = m * (extendedWindowData.length - 1) + b
this.boundaryPoints.u2 = [tLast, $2 + stdDev]
this.boundaryPoints.l2 = [tLast, $2 - stdDev]
}
}
}
Loading