Last active
April 2, 2023 10:10
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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