aboutsummaryrefslogtreecommitdiff
path: root/internal/cryptomarket/money.go
blob: 8154623fe4224b1f8a3075d56685f1597c38a909 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// Package cryptomarket implements a fictional-credit exchange, not real orders.
package cryptomarket

import (
	"fmt"
	"math"
	"math/big"
	"regexp"
	"strconv"
)

const MaxMoney int64 = 1_000_000_000_000
const MaxPositions = 50

// Decimal strings survive YAML/JSON without a float conversion. In particular,
// neither SHIB prices nor fractional BTC quantities are rounded to credits.
var decimalPattern = regexp.MustCompile(`^(0|[1-9][0-9]{0,14})(\.[0-9]{1,18})?$`)

func Decimal(s string) (*big.Rat, error) {
	if !decimalPattern.MatchString(s) {
		return nil, fmt.Errorf("use a positive decimal (no exponent), at most 15 integer and 18 fractional digits")
	}
	r, ok := new(big.Rat).SetString(s)
	if !ok || r.Sign() <= 0 {
		return nil, fmt.Errorf("amount must be positive")
	}
	return r, nil
}

func rat(s string) *big.Rat      { r, _ := new(big.Rat).SetString(s); return r }
func mul(a, b *big.Rat) *big.Rat { return new(big.Rat).Mul(a, b) }
func add(a, b *big.Rat) *big.Rat { return new(big.Rat).Add(a, b) }
func sub(a, b *big.Rat) *big.Rat { return new(big.Rat).Sub(a, b) }
func integer(n int64) *big.Rat   { return new(big.Rat).SetInt64(n) }

// Round rounds the complete cash amount once, nearest credit, ties away from
// zero. Never round per coin, and never silently clamp an overflow.
func Round(r *big.Rat) (int64, error) {
	if r == nil {
		return 0, fmt.Errorf("invalid amount")
	}
	a := new(big.Rat).Abs(r)
	n, rem := new(big.Int), new(big.Int)
	n.QuoRem(a.Num(), a.Denom(), rem)
	if rem.Lsh(rem, 1).Cmp(a.Denom()) >= 0 {
		n.Add(n, big.NewInt(1))
	}
	if !n.IsInt64() || n.Int64() > MaxMoney {
		return 0, fmt.Errorf("amount exceeds exchange accounting limit")
	}
	v := n.Int64()
	if r.Sign() < 0 {
		v = -v
	}
	return v, nil
}
func Sum(a, b int64) (int64, error) {
	if a < -MaxMoney || a > MaxMoney || b < -MaxMoney || b > MaxMoney || a+b < -MaxMoney || a+b > MaxMoney {
		return 0, fmt.Errorf("exchange balance limit exceeded")
	}
	return a + b, nil
}
func floatRat(f float64) (*big.Rat, error) {
	if math.IsNaN(f) || math.IsInf(f, 0) || f < 0 {
		return nil, fmt.Errorf("invalid model value")
	}
	r, ok := new(big.Rat).SetString(strconv.FormatFloat(f, 'f', 18, 64))
	if !ok {
		return nil, fmt.Errorf("invalid model value")
	}
	return r, nil
}