Skip to content

Instantly share code, notes, and snippets.

@dacr
Last active April 2, 2023 10:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dacr/9f705d56253e4ba7311f001941e5ce93 to your computer and use it in GitHub Desktop.
Save dacr/9f705d56253e4ba7311f001941e5ce93 to your computer and use it in GitHub Desktop.
Simple stock processing to compute global earnings and stock palmares / published by https://github.com/dacr/code-examples-manager #b9379317-2ff5-488c-907c-958cb8a2ca46/fb6dcc3924f275d598dd61d17e43503642bde85c
// summary : Simple stock processing to compute global earnings and stock palmares
// keywords : scala, stocks, data, dataprocessing
// publish : gist
// authors : David Crosson
// license : Apache NON-AI License Version 2.0 (https://raw.githubusercontent.com/non-ai-licenses/non-ai-licenses/main/NON-AI-APACHE2)
// id : b9379317-2ff5-488c-907c-958cb8a2ca46
// created-on : 2020-12-05T14:33:44Z
// managed-by : https://github.com/dacr/code-examples-manager
// execution : scala ammonite script (http://ammonite.io/) - run as follow 'amm scriptname.sc'
import $ivy.`com.github.pathikrit::better-files:3.9.1`
import $ivy.`org.json4s::json4s-jackson:3.6.10`
import $ivy.`org.json4s::json4s-ext:3.6.10`
import better.files._
import org.json4s._
import org.json4s.DefaultFormats
import org.json4s.ext.{JavaTimeSerializers, JavaTypesSerializers}
import org.json4s.jackson.Serialization.read
import java.time.OffsetDateTime
import java.nio.charset.Charset
val inputCharset = Charset.forName("UTF-8")
val inputFile = file"stock-operations.json"
object OperationKind {
val buy="buy"
val sell="sell"
val dividend="dividend"
val fee="fee"
val ignored="ignored"
}
case class Operation(
date: OffsetDateTime,
kind: String,
quantity: Int,
code: String,
codeISIN: String,
label: String,
account: String,
totalAmount: Double
)
implicit val jsonFormat = DefaultFormats.lossless ++ JavaTimeSerializers.all ++ JavaTypesSerializers.all
val operations = read[List[Operation]](inputFile.contentAsString(inputCharset))
// =====================================================================================================================
// Let's compute some interesting stuff
println("---------------------- operation counts ----------------------")
println(s"operations count : ${operations.size}")
operations.groupBy(_.account).foreach{case (account, accountOperations) => println(s" for $account : ${accountOperations.size}")}
println("---------------------- french taxes ----------------------")
val totalFees = operations.collect{case op if op.kind == OperationKind.fee => op.totalAmount}.sum
println(f"Paid fees : $totalFees%#,.2f (TTF / 2012-08-01)")
println("---------------------- dividends ----------------------")
val dividends = operations.collect{case op if op.kind == OperationKind.dividend => op.totalAmount}.sum
println(f"Dividends : $dividends%#,.2f (TTF / 2012-08-01)")
println("---------------------- earnings (fees and dividends included) ----------------------")
case class SnapKey(account:String, code:String)
case class Snapshot(quantity:Int,costPrice:Double)
case class Earning(account:String, code:String, date:OffsetDateTime, amount:Double, kind:String)
type State=(Map[SnapKey,Snapshot], List[Earning]) // (snapshots, double)
val emptyState=(Map.empty[SnapKey,Snapshot], List.empty[Earning])
def processOperationBuy(snapshots:Map[SnapKey,Snapshot], earnings:List[Earning], op:Operation):State = {
val key=SnapKey(op.account, op.code)
val newSnapshot =
snapshots.get(key) match {
case None => Snapshot(op.quantity, op.totalAmount)
case Some(oldSnap) => Snapshot(oldSnap.quantity + op.quantity, oldSnap.costPrice + op.totalAmount)
}
(snapshots + (key -> newSnapshot), earnings)
}
def processOperationSell(snapshots:Map[SnapKey,Snapshot], earnings:List[Earning], op:Operation):State = {
val key=SnapKey(op.account, op.code)
snapshots.get(key) match {
case None =>
println(s"** MISSING BUY INFORMATION FOR $op **")
(snapshots,earnings) // TODO - Orphan transaction MEANING Missing DATA !
case Some(oldSnapshot) =>
val referenceValue = oldSnapshot.costPrice*op.quantity/oldSnapshot.quantity
val newSnapshot= Snapshot(oldSnapshot.quantity - op.quantity, oldSnapshot.costPrice-referenceValue)
val earning = Earning(op.account, op.code, op.date, op.totalAmount + referenceValue, op.kind)
(snapshots + (key->newSnapshot), earning::earnings)
}
}
def processOperations(currentState:State, op:Operation):State = {
val (snapshots, earnings) = currentState
op.kind match {
case OperationKind.buy => processOperationBuy(snapshots, earnings, op)
case OperationKind.sell => processOperationSell(snapshots, earnings, op)
case OperationKind.dividend => (snapshots, Earning(op.account, op.code, op.date,op.totalAmount,op.code)::earnings)
case OperationKind.fee => (snapshots, Earning(op.account, op.code, op.date, op.totalAmount, op.code)::earnings)
case _ => (snapshots,earnings)
}
}
val earnings =
operations
.sortBy(_.date) // VERY IMPORTANT
.foldLeft(emptyState)(processOperations) match {case (_, cumulativeEarnings) => cumulativeEarnings }
val totalEarnings = earnings.map(_.amount).sum
println(f"global earnings : $totalEarnings%#,.2f")
val labelByCode = operations.map(op => op.code -> op.label).toMap
println("detailed earnings :")
earnings
.groupBy(_.code)
.view
.mapValues(_.map(_.amount).sum)
.to(List)
.sortBy{case (_,value) => -value }
.foreach{case (code, value) => println(f" $value%#,10.2f ${labelByCode(code)}")}
println(s"detailed earnings by account :")
earnings
.groupBy(_.account)
.foreach{case (account, accountEarnings) =>
println(f" account $account => ${accountEarnings.map(_.amount).sum}%#,.2f")
accountEarnings // TODO - refactor to avoid duplications
.groupBy(_.code)
.view
.mapValues(_.map(_.amount).sum)
.to(List)
.sortBy{case (_,value) => -value }
.foreach{case (code, value) => println(f" $value%#,10.2f ${labelByCode(code)}")}
}
// TODO - Augmentation de capital / splits / ... /!\
println(s"latest cost prices per stock")
val costPricesByCode =
operations
.groupBy(_.code)
.collect{case (code,operations) =>
operations
.filter(_.kind == OperationKind.sell)
.maxByOption(_.date.toEpochSecond)
.map(op => code -> op.totalAmount / op.quantity)
}.flatten
costPricesByCode
.map{case (code, costPrice) => (labelByCode(code)+" ("+code+") ") -> costPrice}
.toList
.sortBy{case (label, _) => label}
.foreach{ case (label, costPrice) =>
println(f" $costPrice%#,10.2f $label")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment