I want to calculate the intersection point, in time, of 2 or 3 moving averages.

To solve this problem I decided to transform the moving average(ma) data into geometrical lines and calculate the intersection point via the formula for lines in the slope/intercept form.

X coordinate is the unix timestamp of the ma, the Y coordinate is the value of the ma.

Converting the intersection point to `datetime`

format gives me the exact time of intersection.

The data is coming in from a websocket and it is based on a granularity setting.

In order to build the line representations of the moving averages a logic layer is needed to correctly set and update the X,Y coordinates of the lines.

Would like to know if the mutation operations on the line data can be written in a cleaner way.

Also any other improvement suggestions are welcome.

`geometry.go`

```
package geometry
//Line represents a geometrical line
type Line struct {
PointA Point
PointB Point
slope float64
intercept float64
}
func NewLine(a Point, b Point) Line {
line := Line{
PointA: a,
PointB: b,
}
line.calculateSlope()
line.calculateIntercept()
return line
}
func (l *Line) calculateSlope() {
l.slope = (l.PointA.Y - l.PointB.Y) / (l.PointA.X - l.PointB.X)
}
func (l *Line) Slope() float64 {
return l.slope
}
func (l *Line) calculateIntercept() {
l.intercept = l.PointA.Y - (l.slope * l.PointA.X)
}
//Point represents a single Point on the x,y axis
type Point struct {
X,
Y float64
}
//IntersctionPoint return the intersection point of two lines.
//Calculation uses slope/intercept line form
func IntersectionPoint(l1, l2 Line) Point {
//line slope intercept form equation
//y - y coordinate, x - x coordinate
//m - slope, b intercept
//y = mx + b
//intersection equation for line definitions y = 3x-3 and y = 2.3x+4
//(3x-3 = 2.3x+4), (3x-2.3x = 4+3)
//0.7x = 7, (x = 7/0.7), x = 10
leftSide := l1.slope + (l2.slope * -1)
rightSide := l2.intercept + (l1.intercept * -1)
x := rightSide / leftSide
y := l1.slope*x + l1.intercept
return Point{X: x, Y: y}
}
```

`intersector.go`

```
package intersector
import (
"fmt"
"intersector/geometry"
"time"
)
const (
up = 1
down = -1
)
type Average struct {
Value float64
Timestamp int
}
//intersector builds lines from the received moving average data
//and calculates their intersection point. max 3 lines
type intersector struct {
input chan ()Average
}
func NewIntersector(input chan ()Average) intersector {
return intersector{
input: input,
}
}
//point is a wrapper around geometry.Point
//adds timestamp field needed for tracking and placing
//moving average values as x,y coordinates
type point struct {
point geometry.Point
timestamp int
}
func newPoint(x, y float64, timestamp int) point {
return point{
point: geometry.Point{
X: x,
Y: y,
},
timestamp: timestamp,
}
}
//line is a wrapper around geometry.Line
//adds helper fields for capturing point data
//from data stream
type line struct {
geometry geometry.Line
pointA point
pointB point
direction int
}
func newLine(a, b point) *line {
return &line{
geometry: geometry.NewLine(a.point, b.point),
pointA: a,
pointB: b,
}
}
//update updates pointA or pointB of a line base on the timestamp of the average
func (line *line) update(average Average) bool {
//data refers to pointA of the line definition
//will not trigger once pointB is defined
if line.pointA.timestamp == average.Timestamp {
line.pointA.point.Y = average.Value
return true
}
//checks if pointB is defined yet, if not, inititialises pointB and creates the line
if line.pointB == (point{}) {
line.pointB = newPoint(float64(average.Timestamp), average.Value, average.Timestamp)
line.geometry = geometry.NewLine(line.pointA.point, line.pointB.point)
return true
}
if average.Timestamp == line.pointB.timestamp {
line.pointB.point.Y = average.Value
return true
}
//collecting data on pointB has finished. line is complete
//return false as there is nothing to update for this line
if average.Timestamp != line.pointB.timestamp {
return false
}
return true
}
func (l *line) setDirection(direction int) {
l.direction = direction
}
func getDirection(line geometry.Line) int {
horizon := 0.00
if line.Slope() <= horizon {
return down
}
return up
}
func (z intersector) Calculate() {
incompleteLines := make(map(int)*line)
completeLines := make(map(int)*line)
for {
if averages, ok := <-z.input; ok {
for index, average := range averages {
if line, ok := incompleteLines(index); !ok {
pointA := newPoint(float64(average.Timestamp), average.Value, average.Timestamp)
pointB := point{}
incompleteLines(index) = newLine(pointA, pointB)
continue
} else {
if updated := line.update(average); !updated {
pointA := newPoint(float64(line.pointA.timestamp), line.pointA.point.Y, line.pointA.timestamp)
pointB := newPoint(float64(line.pointB.timestamp), line.pointB.point.Y, line.pointB.timestamp)
completeLines(index) = newLine(pointA, pointB)
incompleteLines(index) = newLine(line.pointB, point{})
}
}
}
//QUESTION::
//Can the same be done with a for loop avoiding the code duplication for lines A and B?
switch len(completeLines) {
case 2:
lineA := completeLines(0)
lineB := completeLines(1)
intersection := geometry.IntersectionPoint(lineA.geometry, lineB.geometry)
//check if the intersection point is within the origin coordinates of the lines
//intersections that occur outside of the lline definition are not important
if intersection.X >= lineA.geometry.PointA.X && intersection.X <= lineB.geometry.PointB.X {
//only print intersections that do not occur in the same direction. lines can intersect multiple times going upward or downward
if lineA.direction != getDirection(lineA.geometry) {
lineA.setDirection(getDirection(lineA.geometry))
fmt.Printf("Line1 interesects Line2 at: %v Y: %v X: %v Direction: %vn", t(intersection.X), intersection.Y, intersection.X, getDirection(lineA.geometry))
}
}
case 3:
lineA := completeLines(0)
lineB := completeLines(1)
lineC := completeLines(2)
intersection1 := geometry.IntersectionPoint(lineA.geometry, lineB.geometry)
intersection2 := geometry.IntersectionPoint(lineA.geometry, lineC.geometry)
intersection3 := geometry.IntersectionPoint(lineB.geometry, lineC.geometry)
//check if the intersection point is within the origin coordinates of the lines
//intersections that occur outside of the lline definition are not important
if intersection1.X >= lineA.geometry.PointA.X && intersection1.X <= lineB.geometry.PointB.X {
//only print intersections that do not occur in the same direction. lines can intersect multiple times going upward or downward
if lineA.direction != getDirection(lineA.geometry) {
lineA.setDirection(getDirection(lineA.geometry))
fmt.Printf("Line1 interesects Line2 at: %v Y: %v X: %v Direction: %vn", t(intersection1.X), intersection1.Y, intersection1.X, getDirection(lineA.geometry))
}
}
if intersection2.X >= lineA.geometry.PointA.X && intersection2.X <= lineC.geometry.PointB.X {
if lineC.direction != getDirection(lineC.geometry) {
lineC.setDirection(getDirection(lineC.geometry))
fmt.Printf("Line1 interesects Line3 at: %v Y: %v X: %v Direction: %vn", t(intersection2.X), intersection2.Y, intersection2.X, getDirection(lineA.geometry))
}
}
if intersection3.X >= lineB.geometry.PointA.X && intersection3.X <= lineC.geometry.PointB.X {
if lineB.direction != getDirection(lineB.geometry) {
lineB.setDirection(getDirection(lineB.geometry))
fmt.Printf("Line2 interesects Line3 at: %v Y: %v X: %v Direction: %vn", t(intersection3.X), intersection3.Y, intersection3.X, getDirection(lineB.geometry))
}
}
}
} else {
return
}
}
}
//converts unix time to string time
func t(timestamp float64) string {
t := int64(timestamp / 1000)
tm := time.Unix(t, 0)
return tm.Format(time.RFC3339)
}
```