first version

This commit is contained in:
2026-06-07 21:01:40 +00:00
parent ee7167accf
commit f19bab1100
79 changed files with 10355 additions and 145 deletions
+117
View File
@@ -0,0 +1,117 @@
package money
import (
"errors"
"github.com/shopspring/decimal"
pb "github.com/russianinvestments/invest-api-go-sdk/proto"
)
var (
ErrInvalidTick = errors.New("tick must be positive")
ErrInvalidBase = errors.New("base must be positive")
)
type RoundMode int
const (
RoundNearest RoundMode = iota
RoundFloor
RoundCeil
)
func QuotationToDecimal(q *pb.Quotation) decimal.Decimal {
if q == nil {
return decimal.Zero
}
return decimal.NewFromInt(q.GetUnits()).Add(decimal.New(int64(q.GetNano()), -9))
}
func DecimalToQuotation(d decimal.Decimal) *pb.Quotation {
units := d.Truncate(0)
nano := d.Sub(units).Mul(decimal.NewFromInt(1_000_000_000)).Round(0)
if nano.Equal(decimal.NewFromInt(1_000_000_000)) {
units = units.Add(decimal.NewFromInt(1))
nano = decimal.Zero
}
if nano.Equal(decimal.NewFromInt(-1_000_000_000)) {
units = units.Sub(decimal.NewFromInt(1))
nano = decimal.Zero
}
nanoPart := nano.IntPart()
if nanoPart < -999_999_999 || nanoPart > 999_999_999 {
panic("decimal quotation nano is out of protobuf range")
}
return &pb.Quotation{
Units: units.IntPart(),
Nano: int32(nanoPart), // #nosec G115 -- nanoPart is bounded above.
}
}
func MoneyValueToDecimal(v *pb.MoneyValue) decimal.Decimal {
if v == nil {
return decimal.Zero
}
return decimal.NewFromInt(v.GetUnits()).Add(decimal.New(int64(v.GetNano()), -9))
}
func Bps(part, base decimal.Decimal) (decimal.Decimal, error) {
if !base.IsPositive() {
return decimal.Zero, ErrInvalidBase
}
return part.Div(base).Mul(decimal.NewFromInt(10_000)), nil
}
func FromBps(bps decimal.Decimal) decimal.Decimal {
return bps.Div(decimal.NewFromInt(10_000))
}
func RoundToTick(price, tick decimal.Decimal, mode RoundMode) (decimal.Decimal, error) {
if !tick.IsPositive() {
return decimal.Zero, ErrInvalidTick
}
steps := price.Div(tick)
switch mode {
case RoundFloor:
steps = steps.Floor()
case RoundCeil:
steps = steps.Ceil()
default:
steps = steps.Round(0)
}
return steps.Mul(tick), nil
}
func Min(values ...decimal.Decimal) decimal.Decimal {
if len(values) == 0 {
return decimal.Zero
}
min := values[0]
for _, value := range values[1:] {
if value.LessThan(min) {
min = value
}
}
return min
}
func Max(values ...decimal.Decimal) decimal.Decimal {
if len(values) == 0 {
return decimal.Zero
}
max := values[0]
for _, value := range values[1:] {
if value.GreaterThan(max) {
max = value
}
}
return max
}
func Abs(value decimal.Decimal) decimal.Decimal {
if value.IsNegative() {
return value.Neg()
}
return value
}
+39
View File
@@ -0,0 +1,39 @@
package money
import (
"testing"
"github.com/shopspring/decimal"
)
func d(raw string) decimal.Decimal {
v, err := decimal.NewFromString(raw)
if err != nil {
panic(err)
}
return v
}
func TestRoundToTick(t *testing.T) {
tests := []struct {
price string
tick string
mode RoundMode
want string
}{
{"10.12346", "0.0001", RoundNearest, "10.1235"},
{"10.126", "0.01", RoundFloor, "10.12"},
{"10.126", "0.01", RoundCeil, "10.13"},
{"10.24", "0.5", RoundNearest, "10"},
{"10.26", "0.5", RoundNearest, "10.5"},
}
for _, tt := range tests {
got, err := RoundToTick(d(tt.price), d(tt.tick), tt.mode)
if err != nil {
t.Fatal(err)
}
if !got.Equal(d(tt.want)) {
t.Fatalf("RoundToTick(%s,%s)=%s want %s", tt.price, tt.tick, got, tt.want)
}
}
}