0
\$\begingroup\$

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: %v\n", 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: %v\n", 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: %v\n", 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: %v\n", 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)
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.