package main
import (
"context"
"log"
"os"
"strconv"
"strings"
"log/slog"
"github.com/stamm/jetlend/pkg"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
cron "github.com/robfig/cron/v3"
)
var keyboard = tgbotapi.NewReplyKeyboard(
tgbotapi.NewKeyboardButtonRow(
tgbotapi.NewKeyboardButton("action"),
tgbotapi.NewKeyboardButton("secondary"),
),
)
func main() {
slog.Info("Start daemon")
bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN"))
if err != nil {
panic(err)
}
bot.Debug = true
cronDailyStart(bot)
cronHourlyStart(bot)
// logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
logger := log.New(os.Stderr, "[tg]: ", log.LstdFlags|log.Lmsgprefix)
tgbotapi.SetLogger(logger)
updateConfig := tgbotapi.NewUpdate(0)
updateConfig.Timeout = 86400
updates := bot.GetUpdatesChan(updateConfig)
for update := range updates {
if update.Message == nil {
continue
}
reply := "Для старта нужно ввести куку sessionid из браузера. Эта кука очень приватна. Мы не храним введёную куку, поэтому её нужно указывать каждый раз.\nTo start, you must enter the sessionid cookie from your browser. This cookie is very private. We don't store entered cookie. You need to enter cookie sessionid every time."
isErr, isStart := false, false
if update.Message.Text != "/start" {
switch update.Message.Text {
case "action":
data := strings.Split(os.Getenv("JETLEND_CFG"), ";")
cfg := make(map[int64][]string, len(data))
for _, v := range data {
s := strings.Split(v, "=")
n, err := strconv.Atoi(s[0])
if err != nil {
continue
}
sids := strings.Split(s[1], ",")
cfg[int64(n)] = sids
}
log.Printf("+%v", cfg)
ctx := context.Background()
sids, ok := cfg[update.Message.From.ID]
if ok {
var have bool
reply, have, err = pkg.WhatBuy(ctx, sids, false, true)
if err != nil {
reply = "Error: " + err.Error()
isErr = true
}
if !have {
reply = ""
}
}
case "secondary":
data := strings.Split(os.Getenv("JETLEND_CFG"), ";")
cfg := make(map[int64][]string, len(data))
for _, v := range data {
s := strings.Split(v, "=")
n, err := strconv.Atoi(s[0])
if err != nil {
continue
}
sids := strings.Split(s[1], ",")
cfg[int64(n)] = sids
}
log.Printf("+%v", cfg)
ctx := context.Background()
sids, ok := cfg[update.Message.From.ID]
if ok {
var have bool
reply, have, err = pkg.SecondaryMarket(ctx, sids, false, true)
if err != nil {
reply = "Error: " + err.Error()
isErr = true
}
if !have {
reply = ""
}
}
default:
var err error
cookies := strings.Split(update.Message.Text, ",")
reply, err = pkg.Run(context.Background(), cookies, false, true)
if err != nil {
reply = "Error: " + err.Error()
isErr = true
}
}
} else {
isStart = true
}
log.Printf("reply: %s", reply)
if reply == "" {
continue
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, reply)
msg.ReplyToMessageID = update.Message.MessageID
msg.ReplyMarkup = keyboard
if !isErr && !isStart {
msg.ParseMode = "MarkdownV2"
}
if _, err := bot.Send(msg); err != nil {
log.Printf("can't send message: %s", err)
}
}
}
func cronDailyStart(bot *tgbotapi.BotAPI) error {
logger := log.New(os.Stderr, "[cron]: ", log.LstdFlags|log.Lmsgprefix)
c := cron.New(cron.WithLogger(cron.VerbosePrintfLogger(logger)))
schedule := os.Getenv("JETLEND_SCHEDULE")
if schedule == "" {
schedule = "0 21 * * *"
}
_, err := c.AddFunc(schedule, func() { sendDaily(bot) })
c.Start()
return err
}
func cronHourlyStart(bot *tgbotapi.BotAPI) error {
logger := log.New(os.Stderr, "[cron]: ", log.LstdFlags|log.Lmsgprefix)
c := cron.New(cron.WithLogger(cron.VerbosePrintfLogger(logger)))
schedule := "19 6-17 * * *"
_, err := c.AddFunc(schedule, func() { sendHourly(bot) })
c.Start()
return err
}
func sendDaily(bot *tgbotapi.BotAPI) {
log.Printf("start sending by cron\n")
data := strings.Split(os.Getenv("JETLEND_CFG"), ";")
d, ok := os.LookupEnv("JETLEND_DAYS")
if !ok {
d = "31"
}
days, err := strconv.Atoi(d)
if err != nil {
log.Printf("error converting int to string: %s", err)
days = 31
}
cfg := make(map[int64][]string, len(data))
for _, v := range data {
s := strings.Split(v, "=")
n, err := strconv.Atoi(s[0])
if err != nil {
continue
}
sids := strings.Split(s[1], ",")
cfg[int64(n)] = sids
}
log.Printf("+%v", cfg)
ctx := context.Background()
for chatID, sids := range cfg {
isErr := false
reply, err := pkg.Run(ctx, sids, false, true)
if err != nil {
reply = "Error: " + err.Error()
isErr = true
}
expectMsg, err := pkg.ExpectAmount(ctx, sids, false, days)
if err != nil {
reply += "Error: " + err.Error()
isErr = true
} else {
reply += "\n" + expectMsg
}
msg := tgbotapi.NewMessage(chatID, reply)
if !isErr {
msg.ParseMode = "MarkdownV2"
}
if _, err := bot.Send(msg); err != nil {
log.Printf("error send for chat_id %d: %s", chatID, err)
}
}
}
func sendHourly(bot *tgbotapi.BotAPI) {
log.Printf("start sending by cron\n")
data := strings.Split(os.Getenv("JETLEND_CFG"), ";")
cfg := make(map[int64][]string, len(data))
for _, v := range data {
s := strings.Split(v, "=")
n, err := strconv.Atoi(s[0])
if err != nil {
continue
}
sids := strings.Split(s[1], ",")
cfg[int64(n)] = sids
}
log.Printf("+%v", cfg)
ctx := context.Background()
for chatID, sids := range cfg {
isErr := false
reply, have, err := pkg.WhatBuy(ctx, sids, false, true)
if err != nil {
reply = "Error: " + err.Error()
isErr = true
}
if !have {
continue
}
msg := tgbotapi.NewMessage(chatID, reply)
if !isErr {
msg.ParseMode = "MarkdownV2"
}
if _, err := bot.Send(msg); err != nil {
log.Printf("error send for chat_id %d: %s", chatID, err)
}
}
}
package main
import (
"context"
"flag"
"fmt"
"os"
"strconv"
"strings"
"github.com/stamm/jetlend/pkg"
)
func main() {
ctx := context.Background()
// ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
// defer stop()
cookies := strings.Split(os.Getenv("JETLEND_COOKIE"), ",")
d, ok := os.LookupEnv("JETLEND_DAYS")
if !ok {
d = "7"
}
days, err := strconv.Atoi(d)
if err != nil {
panic(err)
}
var mode = flag.String("m", "", "stats: empty (stat), expect")
flag.Parse()
switch *mode {
case "", "stat":
msg, err := pkg.Run(ctx, cookies, true, false)
if err != nil {
panic(err)
}
fmt.Print(msg)
case "expect":
msg, err := pkg.ExpectAmount(ctx, cookies, true, days)
if err != nil {
panic(err)
}
fmt.Print(msg)
case "what_buy":
msg, have, err := pkg.WhatBuy(ctx, cookies, true, false)
if err != nil {
panic(err)
}
if have {
fmt.Print(msg)
}
case "secondary":
msg, have, err := pkg.SecondaryMarket(ctx, cookies, true, false)
if err != nil {
panic(err)
}
if have {
fmt.Print(msg)
}
}
// stop()
// fmt.Printf("ctx done: %s", <-ctx.Done())
}
package main
import (
"encoding/csv"
"fmt"
"io"
"os"
"time"
"github.com/gocarina/gocsv"
)
type Transaction struct {
Date string `csv:"Дата"`
Type string `csv:"Тип операции"`
Company string `csv:"Компания"`
Inn string `csv:"ИНН"`
In float64 `csv:"Приход"`
Out float64 `csv:"Расход"`
Debt float64 `csv:"Основной долг"`
Income float64 `csv:"Доход"`
}
type FloatCSV float64
func (f *FloatCSV) MarshalCSV() (string, error) {
return fmt.Sprintf("%.2f", float64(*f)), nil
}
// TYPE;DATE;TICKER;QUANTITY;PRICE;FEE;NKD;NOMINAL;CURRENCY;FEE_CURRENCY;NOTE;LINK_ID;TRADE_SYSTEM_ID
type IntelInvest struct {
Type string `csv:"TYPE"`
Date string `csv:"DATE"`
Ticker string `csv:"DATE"`
Quantity string `csv:"QUANTITY"`
Price FloatCSV `csv:"PRICE"`
Fee string `csv:"FEE"`
Nkd string `csv:"NKD"`
Nominal string `csv:"NOMINAL"`
Currency string `csv:"CURRENCY"`
FeeCurrency string `csv:"FEE_CURRENCY"`
Note string `csv:"NOTE"`
LinkId string `csv:"LINK_ID"`
TradeSystemId string `csv:"TRADE_SYSTEM_ID"`
}
// type IntelInvest struct {
// Type string
// Date string
// Ticker string
// Quantity string
// Price FloatCSV
// Fee string
// Nkd string
// Nominal string
// Currency string
// FeeCurrency string
// Note string
// LinkId string
// TradeSystemId string
// }
type Default struct {
Company string
INN string
Date string
}
func main() {
in, err := os.Open("transactions-2.csv")
if err != nil {
panic(err)
}
defer in.Close()
transactions := []*Transaction{}
if err := gocsv.UnmarshalFile(in, &transactions); err != nil {
panic(err)
}
// for _, v := range []int{0, 11, 63, 122, 953, 3031, 4416, 7182, 7429} {
// fmt.Printf("%+v\n", transactions[v])
// }
// for _, client := range clients {
// if client.Type == "investment" ||
// client.Type == "collection" ||
// client.Type == "sale" ||
// client.Type == "default" ||
// client.Type == "contract" ||
// client.Type == "purchase" ||
// client.Type == "payment" {
// continue
// }
// fmt.Printf("=%+v\n", client)
// break
// }
sum := FloatCSV(0)
date := ""
defaults := make(map[string]Default)
converted := make([]IntelInvest, 0, 10)
for i := len(transactions) - 1; i >= 0; i-- {
transaction := transactions[i]
// if transaction.Type != "investment" {
// continue
// }
if transaction.Type == "default" {
defaults[transaction.Inn] = Default{
Company: transaction.Company,
Date: transaction.Date,
INN: transaction.Inn,
}
}
if v, ok := convert(*transaction); ok {
if v.Type == "INCOME" && v.Note != "Бонусы" {
if v.Date != date || i == 0 {
vSum := v
if date != "" && sum != 0 {
vSum.Price = FloatCSV(sum)
}
converted = append(converted, vSum)
date = v.Date
sum = FloatCSV(0)
}
sum += v.Price
} else {
converted = append(converted, v)
}
}
// fmt.Printf("=%+v\n", transaction)
}
// fmt.Printf("converted: %+v\n", converted)
// fmt.Printf("defaults: %+v\n", defaults)
defSum := make(map[string]float64, len(defaults))
for _, v := range transactions {
if _, ok := defaults[v.Inn]; !ok {
continue
}
// + contract, purchase
// - sale, payment
switch v.Type {
case "contract":
defSum[v.Inn] += v.Out
case "purchase":
defSum[v.Inn] += v.Debt
case "payment":
defSum[v.Inn] -= v.Debt
case "sale":
defSum[v.Inn] -= v.Debt
}
}
// fmt.Printf("defSum: %+v\n", defSum)
for _, v := range defaults {
// fmt.Printf("inn: %s, company: %s, date %s, sum: %0.2f\n", k, v.Company, v.Date, defSum[v.INN])
converted = append(converted, MakeII("LOSS", "Дефолт "+v.Company, convertDate(v.Date), defSum[v.INN]))
}
gocsv.SetCSVWriter(func(out io.Writer) *gocsv.SafeCSVWriter {
csvout := gocsv.NewSafeCSVWriter(csv.NewWriter(out))
csvout.Comma = ';'
return csvout
})
csv, err := gocsv.MarshalStringWithoutHeaders(&converted)
if err != nil {
panic(err)
}
csv = "#CsvFormatVersion:v1\n" + csv
fmt.Println(csv)
}
func convert(a Transaction) (IntelInvest, bool) {
date := convertDate(a.Date)
switch a.Type {
// 12362 payment
// 1219 contract
// 715 purchase
// 166 investment
// 77 sale
// 46 default
// 26 collection
// вводы, выводы, бонусы
case "investment":
if a.Income >= 0.01 {
// fmt.Printf("%+v\n", a)
return MakeII("INCOME", "Бонусы", date, a.Income), true
}
if a.In > 0 {
return MakeII("MONEYDEPOSIT", "", date, a.In), true
} else {
return MakeII("MONEYWITHDRAW", "", date, -a.Out), true
}
// прибыль
case "payment":
if a.Income > 0 {
return MakeII("INCOME", "Прибыль", date, a.Income), true
}
// продажа
case "sale":
if a.Income > 0 {
return MakeII("INCOME", "Продажа займа", date, a.Income), true
}
if a.Income < 0 {
return MakeII("LOSS", "Продажа займа", date, a.Income), true
}
// покупка
case "purchase":
if a.Income > 0 {
return MakeII("INCOME", "Покупка займа", date, a.Income), true
}
if a.Income < 0 {
return MakeII("LOSS", "Покупка займа", date, a.Income), true
}
// same
// 5277,2022-12-08,payment,ИП Шуляка Александр Андреевич,910901600445,27.5,,27.5,0
// 5279,2022-12-08,collection,ИП Шуляка Александр Андреевич,910901600445,27.5,,27.5,0
// case "collection":
// return MakeII("INCOME", "Возврат долга", date, a.In), true
}
return IntelInvest{}, false
}
func MakeII(t, note, date string, price float64) IntelInvest {
return IntelInvest{
Type: t,
Date: date,
Price: FloatCSV(price),
Currency: "RUB",
Note: note,
}
}
func convertDate(in string) string {
parseTime, err := time.Parse("2006-01-02", in)
if err != nil {
panic(err)
}
return parseTime.Format("02.01.2006")
}
/*
invest: in - внёс, out - вывел, income - бонус
contract: out - покупка займа
payment: in = долг + income
collection: debt
default: пустой
+ contract, purchase
- sale
*/
package http
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"github.com/stamm/jetlend/pkg"
)
const (
okCode = http.StatusOK
defaultURL = "https://jetlend.ru/invest/api"
)
// ErrGetJSON is an error getting json
var ErrGetJSON = errors.New("can't get json")
type Jetlend struct {
Timeout time.Duration
URL string
}
var _ HTTP = (*Jetlend)(nil)
func NewJetlend(url string, timeout time.Duration) *Jetlend {
if url == "" {
url = defaultURL
}
return &Jetlend{
URL: url,
Timeout: timeout,
}
}
func (j *Jetlend) getJSON(ctx context.Context, client *http.Client, url, sid string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, j.Timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return []byte{}, fmt.Errorf("couldn't create request for url %s: %w", url, err)
}
req.Header.Set("Cookie", "sessionid="+sid)
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0")
resp, err := client.Do(req)
if err != nil {
return []byte{}, fmt.Errorf("couldn't do request for url %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != okCode {
b, _ := io.ReadAll(resp.Body)
return []byte{}, fmt.Errorf("response code %d: %s", resp.StatusCode, string(b))
}
return io.ReadAll(resp.Body)
}
func (j Jetlend) url(uri string) string {
return j.URL + "/" + uri
}
func (j Jetlend) urlExpect(days int) string {
return j.url(fmt.Sprintf("portfolio/charts/expected_revenue?size=%d", days))
}
// ExpectAmount show returns for loans
func (j Jetlend) ExpectAmount(ctx context.Context, cfg pkg.Config, days int) ([]byte, error) {
if len(cfg.Sids) == 0 {
return []byte{}, errors.New("no config")
}
body, err := j.getJSON(ctx,
http.DefaultClient,
j.urlExpect(days),
cfg.Sids[0])
if err != nil {
return []byte{}, fmt.Errorf("%w for portfolio/charts/expected_revenue: %w", ErrGetJSON, err)
}
return body, nil
}
// func (j Jetlend) LoansPorfolio(ctx context.Context, cfg pkg.Config) error {
// var g errgroup.Group
// for _, sid := range sids {
// sid := sid
// g.Go(func() error {
// body, err := getJSON(ctx, http.DefaultClient, jetURL("portfolio/distribution"), sid)
// if err != nil {
// return fmt.Errorf("%w for portfolio/distribution (1): %w", ErrGetJSON, err)
// }
// sum, values, err2 := extractLoans(body)
// if err2 != nil {
// return fmt.Errorf("couldn't extract loans for portfolio/distribution (1): %w", err2)
// }
// rep.Mu.Lock()
// rep.Sum += sum
// for k, v := range values {
// if _, ok := rep.Values[k]; ok {
// rep.Values[k] += v
// } else {
// rep.Values[k] = v
// }
// }
// rep.Mu.Unlock()
// return nil
// })
// }
// if err := g.Wait(); err != nil {
// return fmt.Errorf("get error for loans: %s", err)
// }
// return nil
// }
// func (j Jetlend) Delayed(ctx context.Context, cfg pkg.Config) error {
// var g errgroup.Group
// for _, sid := range sids {
// sid := sid
// g.Go(func() error {
// body, err := getJSON(ctx, http.DefaultClient, jetURL("portfolio/analytics"), sid)
// if err != nil {
// return fmt.Errorf("%w for portfolio/analytics (2): %w", ErrGetJSON, err)
// }
// delayed, err2 := extractDelayed(body)
// if err2 != nil {
// return fmt.Errorf("couldn't extract delayed for portfolio/analytics (2): %w", err2)
// }
// rep.Mu.Lock()
// // log.Printf("delayed: %.0f\n", delayed)
// rep.Delayed += delayed
// rep.Mu.Unlock()
// return nil
// })
// }
// if err := g.Wait(); err != nil {
// return fmt.Errorf("get error for delayed: %s", err)
// }
// return nil
// }
// func (j Jetlend) Balance(ctx context.Context, rep *Report, sids []string) error {
// var g errgroup.Group
// for _, sid := range sids {
// sid := sid
// g.Go(func() error {
// body, err := getJSON(ctx, http.DefaultClient, jetURL("account/details"), sid)
// if err != nil {
// return fmt.Errorf("%w for account/details (3): %w", ErrGetJSON, err)
// }
// reserved, free, err2 := extractBalance(body)
// if err2 != nil {
// return fmt.Errorf("couldn't extract balance for account/details (3): %w", err2)
// }
// rep.Mu.Lock()
// rep.Reserved += reserved
// rep.Free += free
// rep.Mu.Unlock()
// return nil
// })
// }
// if err := g.Wait(); err != nil {
// return fmt.Errorf("get error for balance: %s", err)
// }
// return nil
// }
// func (j Jetlend) PrimaryMarket(ctx context.Context, rep *Report, sid string) error {
// body, err := getJSON(ctx, http.DefaultClient, jetURL("requests/waiting"), sid)
// if err != nil {
// return fmt.Errorf("%w for requests/waiting: %w", ErrGetJSON, err)
// }
// requests, err2 := extractRequests(body)
// if err2 != nil {
// return fmt.Errorf("couldn't extract requests for requests/waiting: %w", err2)
// }
// rep.Mu.Lock()
// rep.Requests = requests
// rep.Mu.Unlock()
// return nil
// }
// func (j Jetlend) SecondaryMarket(ctx context.Context, rep *Report, sid string) error {
// body, err := getJSON(ctx, http.DefaultClient, jetURL("exchange/loans?limit=1000&offset=0&sort_dir=desc&sort_field=ytm"), sid)
// if err != nil {
// return fmt.Errorf("%w for exchange/loans: %w", ErrGetJSON, err)
// }
// // fmt.Printf("body: %s", string(body))
// secondary, err2 := extractSecondary(body)
// if err2 != nil {
// return fmt.Errorf("couldn't extract requests for exchange/loans: %w", err2)
// }
// rep.Mu.Lock()
// rep.Secondary = secondary
// rep.Mu.Unlock()
// return nil
// }
//go:build !alt
package pkg
import "math"
func muchBuy(req Request, max, sum float64) (float64, string) {
expl := ""
if (max - sum) < max {
expl = "✓"
if req.InvestingAmount.Float64 > 0 {
expl = "-"
}
}
if req.CollectedPercentage < 85 && req.InvestingAmount.Float64 > 0 {
expl = "-"
}
// if invested + reserved more than max sum
// or already collected
if max-sum <= 0 || req.CollectedPercentage >= 100 {
return 0, expl
}
buy := 0.
// if more than 30%
if req.InterestRate >= 0.30 {
// if sum < 1/2 of max (1500)
m := max / 2
if sum < m {
buy = math.Min(m-sum, m)
}
return buy, mark(req, max, sum, buy, expl)
}
// if more than a year
if req.Term >= 390 {
// if sum < 1/2 of max (1500)
if sum < max {
buy = math.Min(max-sum, max)
}
return buy, mark(req, max, sum, buy, expl)
}
// if sum < max (3000)
if sum < max {
buy = max - sum
}
return buy, mark(req, max, sum, buy, expl)
}
func amountBuySecondary(sec Secondary, max, sum float64) (float64, string) {
buy := 0.
// if more than 30%
if sec.YTM >= 0.30 {
// if sum < 1/6 of max (1500)
m := max / 2
if sum < m {
buy = math.Min(m-sum, m)
}
return buy, ""
}
// if more than a year
if sec.Term >= 720 {
// if sum < 1/4 of max (1500)
m := max / 2
if sum < m {
buy = math.Min(m-sum, m)
}
return buy, ""
}
// if sum < max (3000)
if sum < max {
buy = max - sum
}
return buy, ""
}
func mark(req Request, max, sum, buy float64, expl string) string {
if buy < 100 || req.CollectedPercentage < 60 || req.CollectedPercentage > 100 {
return expl
}
expl += "░"
if req.CollectedPercentage > 70 {
expl += "▒"
}
if req.CollectedPercentage > 80 {
expl += "▓"
}
if req.CollectedPercentage > 90 {
expl += "█"
}
return expl
}
package pkg
import "flag"
type Cfg struct {
MinTargetPercent float64 `json:"min_target_percent"`
MinTargetSum float64 `json:"min_target_sum"`
MaxTargetPercent float64 `json:"max_target_percent"`
MaxTargetSum float64 `json:"max_target_sum"`
}
func NewCfg() Cfg {
cfg := DefaultCfg()
var minTargetPercent = flag.Float64("min-target-percent", -1, "min target: 0.1 - 10%")
var minTargetSum = flag.Float64("min-target-sum", -1, "min target sum")
var maxTargetPercent = flag.Float64("max-target-percent", -1, "max target: 0.002 - 0.2%")
var maxTargetSum = flag.Float64("max-target-sum", -1, "max target sum")
flag.Parse()
if *minTargetPercent != -1 {
cfg.MinTargetPercent = *minTargetPercent
}
if *minTargetSum != -1 {
cfg.MinTargetSum = *minTargetSum
}
if *maxTargetPercent != -1 {
cfg.MaxTargetPercent = *maxTargetPercent
}
if *maxTargetSum != -1 {
cfg.MaxTargetSum = *maxTargetSum
}
return cfg
}
func DefaultCfg() Cfg {
return Cfg{
MinTargetPercent: 0.00206,
MinTargetSum: 6001.,
MaxTargetPercent: 0.003,
MaxTargetSum: 9001.,
}
}
package pkg
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
var ErrUnmarshal = errors.New("can't unmarshal")
func extractExpect(body []byte) (Expect, error) {
var expect Expect
err := json.Unmarshal(body, &expect)
if err != nil {
return Expect{}, fmt.Errorf("%w expect: %w", ErrUnmarshal, err)
}
return expect, nil
}
func extractLoans(body []byte) (float64, map[string]float64, error) {
var distr Distr
err := json.Unmarshal(body, &distr)
if err != nil {
return 0., map[string]float64{}, fmt.Errorf("%w loans: %w", ErrUnmarshal, err)
}
sum := float64(0)
comp := make(map[string]float64, len(distr.Data))
count := make(map[string]int)
for _, d := range distr.Data {
com := companyNorm(d.Company)
if comp[com] != 0 {
// log.Printf("spl[0] = %+v\n", spl[0])
if _, ok := count[com]; !ok {
count[com] = 2
} else {
count[com]++
}
}
comp[com] += d.Debt
}
// log.Printf("count = %+v\n", count)
for _, v := range comp {
sum += v
}
return sum, comp, nil
}
func extractDelayed(body []byte) (float64, error) {
var analyt Analytics
err := json.Unmarshal(body, &analyt)
if err != nil {
return .0, fmt.Errorf("%w analitics: %w", ErrUnmarshal, err)
}
return analyt.Data.Status.Delayed, nil
}
func extractBalance(body []byte) (float64, float64, error) {
var details Details
err := json.Unmarshal(body, &details)
if err != nil {
return .0, .0, fmt.Errorf("%w analitics: %w", ErrUnmarshal, err)
}
return details.Data.Balance.Reserved, details.Data.Balance.Free, nil
}
func extractRequests(body []byte) ([]Request, error) {
var waiting Waiting
err := json.Unmarshal(body, &waiting)
if err != nil {
return []Request{}, fmt.Errorf("%w request: %w", ErrUnmarshal, err)
}
return waiting.Requests, nil
}
func extractSecondary(body []byte) ([]Secondary, int, error) {
var secs Secondaries
err := json.Unmarshal(body, &secs)
if err != nil {
return []Secondary{}, 0, fmt.Errorf("%w secondary: %w", ErrUnmarshal, err)
}
return secs.Data, secs.Total, nil
}
func companyNorm(s string) string {
return strings.SplitN(s, "-В", 2)[0]
}
package pkg
import (
"fmt"
"math"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/jedib0t/go-pretty/v6/table"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
const (
// minPercentSecondary = 0.23
minPercent = 0.18
// maxInvest = 6_000
maxInvest = 0
)
func pr(rep *Report, terminal, cli bool) string {
var sb strings.Builder
if !terminal {
// sb.WriteString("<pre>\n")
sb.WriteString("```\n")
}
p := message.NewPrinter(language.Russian)
l := float64(len(rep.Values))
values := make([]float64, 0, len(rep.Values))
for _, v := range rep.Values {
values = append(values, v)
}
sort.Float64s(values)
sum := rep.Sum + rep.Reserved + rep.Free
sb.WriteString(fmt.Sprintf("total = %s\ninvested = %s (%0.1f%%), len = %0.0f, %0.3f%%\ndelayed = %s (%0.1f%%)\nreserved = %s (%0.1f%%)\nfree = %s (%0.1f%%)\nindex = %0.3f%%, %0.2f, %0.3f\n",
p.Sprintf("%0.f", sum),
p.Sprintf("%0.f", rep.Sum), rep.Sum/sum*percent, l, 100./float64(l),
p.Sprintf("%0.f", rep.Delayed), rep.Delayed/sum*percent,
p.Sprintf("%0.f", rep.Reserved), rep.Reserved/sum*percent,
p.Sprintf("%0.f", rep.Free), rep.Free/sum*percent,
index(rep.Values)*100, math.Sqrt(l*0.02*(1-0.02)), math.Sqrt(0.02*(1-0.02)/l)))
// maxTarget := sum*target - 500
// maxTarget := rep.Sum * maxTargetPercent
maxTarget := maxTargetSum
count := 0
countMax := 0
keys := make([]string, 0, len(rep.Values))
for key, v := range rep.Values {
minTarget := minTargetSum
// minTarget := sum*minTargetPercent
if v >= minTarget && v <= maxTarget {
count++
}
if v > maxTarget {
countMax++
}
if false && v <= maxTarget/2 {
// if v <= 8_500 {
continue
}
keys = append(keys, key)
}
sort.SliceStable(keys, func(i, j int) bool {
return rep.Values[keys[i]] < rep.Values[keys[j]]
})
sb.WriteString(
fmt.Sprintf("min target: %s (%.2f%%)\ncount: %d\nmax target: %s (%.2f%%)\ncount :%d\n",
p.Sprintf("%05.f", minTargetSum),
minTargetSum/sum*100,
count,
p.Sprintf("%05.f", maxTarget),
maxTarget/sum*100,
countMax))
for _, v := range keys {
if rep.Values[v] > maxTarget {
sb.WriteString(p.Sprintf("%s %s \n", p.Sprintf("%05.f", rep.Values[v]), v))
}
}
// for _, q := range []int{50, 75, 90, 95, 96, 97, 98, 99, 100} {
for _, q := range []float64{50, 75, 90, 95, 99, 99.5, 99.9, 100} {
c := int(math.Round(l*q/percent)) - 1
if c >= int(l) {
c = int(l) - 1
}
proc := values[c] / rep.Sum
sb.WriteString(fmt.Sprintf("%5.1fq c=%4d(%4d) %s %0.2f%%",
q, c+1, int(l)-c-1, p.Sprintf("%6.f", values[c]), proc*100.0))
if values[c] >= maxTarget {
if terminal {
sb.WriteString("\033[31m")
}
sb.WriteString(fmt.Sprintf(" > %0.1f%%", maxTarget/sum*100))
// } else if proc >= minTargetPercent+0.0001 {
// if terminal {
// sb.WriteString("\033[31m")
// }
// sb.WriteString(fmt.Sprintf(" > %0.1f%%", minTargetPercent*100))
}
if terminal {
sb.WriteString("\033[0m")
}
sb.WriteString("\n")
}
sort.Slice(rep.Requests, func(i, j int) bool {
if rep.Requests[i].InterestRate == rep.Requests[j].InterestRate {
return rep.Requests[i].Company < rep.Requests[j].Company
}
return rep.Requests[i].InterestRate > rep.Requests[j].InterestRate
})
maxStr, ok := os.LookupEnv("JETLEND_MAX")
if !ok {
maxStr = "0"
}
max, err := strconv.Atoi(maxStr)
if err != nil {
panic(err)
}
if max == 0 {
max = int(minTargetSum)
}
sb.WriteString(fmt.Sprintf("max invest sum: %s\n",
p.Sprintf("%d", max)))
if !cli {
sb.WriteString(`✓ already have enough sum
- need to cancel reserved
| more than 60% collected
`)
}
totalBuy := 0
t := table.NewWriter()
t.SetOutputMirror(&sb)
t.AppendHeader(table.Row{"!", "Buy", "Reserved", "Company", "Percent", "Action", "Sum", "Collected", "Total", "Days", "Rating"})
separate := true
for _, req := range rep.Requests {
if req.InterestRate < minPercent {
if separate {
t.AppendSeparator()
separate = false
}
}
if req.InterestRate < minPercent && req.CollectedPercentage < 80 {
continue
}
if req.CollectedPercentage >= 100 {
continue
}
comp := companyNorm(req.LoanName)
sum := 0.
if v, ok := rep.Values[comp]; ok {
sum += v
}
sum += req.InvestingAmount.Float64
// if comp == "СИМБИОЗ" {
// log.Printf("comp %s, req: %+v", comp, req)
// }
s := ""
if math.Abs(float64(max)-sum) < 100 {
continue
}
if int(sum) < max {
// if terminal {
// sb.WriteString("\033[32m")
// }
s = "buy"
} else if int(sum) > max {
// if terminal {
// sb.WriteString("\033[31m")
// }
s = "sell"
}
buyFloat, expl := muchBuy(req, float64(max), sum)
buy := round(buyFloat)
totalBuy += buy
// expl := ""
// if max-int(sum) > int(0.75*float64(max)) &&
// req.Term < 390 &&
// req.InterestRate < 0.3 &&
// req.CollectedPercentage < 100 &&
// req.Rating != "CCC" &&
// req.Amount.Float64 <= 2_500_000 {
// expl = "$"
// }
if s != "" {
t.AppendRows([]table.Row{
{expl, buy, req.InvestingAmount.Float64, comp, fmt.Sprintf("%2.1f%%", req.InterestRate*100), s, int(sum),
int(req.CollectedPercentage),
fmt.Sprintf("%1.1f", req.Amount.Float64/1_000_000),
fmt.Sprintf("%d", req.Term),
req.Rating,
},
})
}
}
t.AppendSeparator()
t.AppendRows([]table.Row{
{"", totalBuy},
})
// t.SetStyle(table.StyleColoredBright)
if !cli {
t.Render()
}
if terminal {
sb.WriteString("\033[0m")
}
if !terminal {
sb.WriteString("```\n")
// sb.WriteString("</pre>\n")
}
return sb.String()
}
func prExpect(exp Expect, terminal bool) string {
var sb strings.Builder
p := message.NewPrinter(language.Russian)
if !terminal {
sb.WriteString("```\n")
}
sum := 0.
for _, d := range exp.Data {
sum += d.Amount
tm := time.Unix(d.Date/1_000, 0)
sb.WriteString(fmt.Sprintf("%02d: %s\n", tm.Day(),
p.Sprintf("%7.f", d.Amount)))
}
sb.WriteString(fmt.Sprintf("su: %s\n", p.Sprintf("%7.f", sum)))
if !terminal {
sb.WriteString("```\n")
// sb.WriteString("</pre>\n")
}
return sb.String()
}
// Herfindahl–Hirschman index
func index(values map[string]float64) float64 {
sum := 0.
for _, v := range values {
sum += v
}
s := 0.
for _, v := range values {
s += v / sum * v / sum
}
return s
}
func round(buy float64) int {
if int(buy)%100 > 0 {
return int(buy) - int(buy)%100
}
return int(buy)
}
package pkg
import (
"fmt"
"log"
"math"
"os"
"sort"
"strconv"
"strings"
)
func needAction(rep *Report, terminal, cli bool) (string, bool) {
var sb strings.Builder
if !terminal {
sb.WriteString("```\n")
}
// p := message.NewPrinter(language.Russian)
sort.Slice(rep.Requests, func(i, j int) bool {
if rep.Requests[i].InterestRate == rep.Requests[j].InterestRate {
return rep.Requests[i].Company < rep.Requests[j].Company
}
return rep.Requests[i].InterestRate > rep.Requests[j].InterestRate
})
maxStr, ok := os.LookupEnv("JETLEND_MAX")
if !ok {
maxStr = "0"
}
max, err := strconv.Atoi(maxStr)
if err != nil {
panic(err)
}
if max == 0 {
max = int(minTargetSum)
}
totalBuy := 0
count := 0
separate := true
for _, req := range rep.Requests {
if req.InterestRate < minPercent {
if separate {
sb.WriteString(strings.Repeat("-", 20) + "\n")
separate = false
}
}
// log.Printf("req.InterestRate = %f, req.CollectedPercentage = %f\n", req.InterestRate, req.CollectedPercentage)
if req.CollectedPercentage >= 100 {
continue
}
comp := companyNorm(req.LoanName)
sum := 0.
if v, ok := rep.Values[comp]; ok {
sum += v
}
sum += req.InvestingAmount.Float64
if math.Abs(float64(max)-sum) < 100 {
continue
}
buyFloat, expl := muchBuy(req, float64(max), sum)
buy := round(buyFloat)
if buy <= 0 && expl != "-" {
continue
}
if expl != "-" && req.CollectedPercentage <= 80 {
continue
}
totalBuy += buy
count++
sb.WriteString(fmt.Sprintf("%s, %d, %.0f, %s, %s, %d, %d%%, %s mln, %s months, %s\n",
expl,
buy,
req.InvestingAmount.Float64,
comp, fmt.Sprintf("%2.1f%%", req.InterestRate*100), int(sum),
int(req.CollectedPercentage),
fmt.Sprintf("%1.1f", req.Amount.Float64/1_000_000),
fmt.Sprintf("%d", req.Term/30),
req.Rating,
))
}
sb.WriteString(fmt.Sprintf("total to buy : %d\n", totalBuy))
if terminal {
sb.WriteString("\033[0m")
}
if !terminal {
sb.WriteString("```\n")
}
log.Printf("count = %d\n", count)
return sb.String(), count > 0
}
package pkg
import (
"fmt"
"log"
"math"
"os"
"sort"
"strconv"
"strings"
)
func needSecondary(rep *Report, terminal, cli bool) (string, bool) {
var sb strings.Builder
if !terminal {
sb.WriteString("```\n")
}
// p := message.NewPrinter(language.Russian)
sort.Slice(rep.Secondary, func(i, j int) bool {
if rep.Secondary[i].YTM == rep.Secondary[j].YTM {
return rep.Secondary[i].Company < rep.Secondary[j].Company
}
return rep.Secondary[i].YTM > rep.Secondary[j].YTM
})
maxStr, ok := os.LookupEnv("JETLEND_MAX")
if !ok {
maxStr = "0"
}
max, err := strconv.Atoi(maxStr)
if err != nil {
panic(err)
}
if max == 0 {
max = int(minTargetSum)
}
totalBuy := 0
count := 0
// separate := true
for _, sec := range rep.Secondary {
if sec.Status == "delayed" {
continue
}
if sec.TermLeft < 60 || float64(sec.TermLeft)/float64(sec.Term) < 0.3 {
continue
}
if sec.FinancialDiscipline <= 0.99 {
continue
}
if sec.PrincipalDebt <= 200 {
continue
}
if sec.YTM < minPercent {
continue
// if separate {
// sb.WriteString(strings.Repeat("-", 20) + "\n")
// separate = false
// }
}
// log.Printf("sec.InterestRate = %f, sec.CollectedPercentage = %f\n", sec.InterestRate, sec.CollectedPercentage)
comp := companyNorm(sec.LoanName)
sum := 0.
if v, ok := rep.Values[comp]; ok {
sum += v
}
if math.Abs(float64(max)-sum) < 100 {
continue
}
buyFloat, expl := amountBuySecondary(sec, float64(max), sum)
buy := round(buyFloat)
if buy <= 0 && expl != "-" {
continue
}
if buy <= 200 {
continue
}
// if expl != "-" {
// continue
// }
totalBuy += buy
count++
sb.WriteString(fmt.Sprintf("%d, %.0f, %.1f, %s, %s, %d, %d, %d months, %s, %0.2f\n",
// expl,
buy, sec.PrincipalDebt,
sec.MinPrice*100,
comp,
fmt.Sprintf("%2.1f%%", sec.YTM*100),
int(sum),
sec.TermLeft/30, sec.Term/30,
sec.Rating,
sec.FinancialDiscipline,
))
}
sb.WriteString(fmt.Sprintf("total to buy : %d\n", totalBuy))
if terminal {
sb.WriteString("\033[0m")
}
if !terminal {
sb.WriteString("```\n")
}
log.Printf("count = %d\n", count)
return sb.String(), count > 0
}
package pkg
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"time"
"log/slog"
"golang.org/x/sync/errgroup"
)
const (
// target = 0.0027
minTargetPercent = 0.00206
minTargetSum = 3_901.
maxTargetPercent = 0.003
maxTargetSum = 4_001.
okCode = 200
percent = 100
timeout = 20 * time.Second
)
type Config struct {
Sids []string
Terminal bool
}
type HTTP interface {
ExpectAmount(ctx context.Context, cfg Config, days int) (string, error)
LoansPorfolio(ctx context.Context, cfg Config) (string, error)
Delayed(ctx context.Context, cfg Config) (int, error)
Balance(ctx context.Context, cfg Config) (int, int, error)
PrimaryMarket(ctx context.Context, cfg Config) (string, error)
SecondaryMarket(ctx context.Context, cfg Config) (string, error)
}
// ErrGetJSON is an error getting json
var ErrGetJSON = errors.New("can't get json")
// ExpectAmount show returns for loans
func ExpectAmount(ctx context.Context, sids []string, terminal bool, days int) (string, error) {
body, err := getJSON(ctx, http.DefaultClient, jetURL(fmt.Sprintf("portfolio/charts/expected_revenue?size=%d", days)), sids[0])
if err != nil {
return "", fmt.Errorf("%w for portfolio/charts/expected_revenue: %w", ErrGetJSON, err)
}
exp, err := extractExpect(body)
if err != nil {
return "", fmt.Errorf("for extract: %w", err)
}
return prExpect(exp, terminal), nil
}
func loans(ctx context.Context, rep *Report, sids []string) error {
var g errgroup.Group
for _, sid := range sids {
sid := sid
g.Go(func() error {
body, err := getJSON(ctx, http.DefaultClient, jetURL("portfolio/distribution"), sid)
if err != nil {
return fmt.Errorf("%w for portfolio/distribution (1): %w", ErrGetJSON, err)
}
sum, values, err2 := extractLoans(body)
if err2 != nil {
return fmt.Errorf("couldn't extract loans for portfolio/distribution (1): %w", err2)
}
rep.Mu.Lock()
rep.Sum += sum
for k, v := range values {
if _, ok := rep.Values[k]; ok {
rep.Values[k] += v
} else {
rep.Values[k] = v
}
}
rep.Mu.Unlock()
return nil
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("get error for loans: %s", err)
}
return nil
}
func delayed(ctx context.Context, rep *Report, sids []string) error {
var g errgroup.Group
for _, sid := range sids {
sid := sid
g.Go(func() error {
body, err := getJSON(ctx, http.DefaultClient, jetURL("portfolio/analytics"), sid)
if err != nil {
return fmt.Errorf("%w for portfolio/analytics (2): %w", ErrGetJSON, err)
}
delayed, err2 := extractDelayed(body)
if err2 != nil {
return fmt.Errorf("couldn't extract delayed for portfolio/analytics (2): %w", err2)
}
rep.Mu.Lock()
// log.Printf("delayed: %.0f\n", delayed)
rep.Delayed += delayed
rep.Mu.Unlock()
return nil
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("get error for delayed: %s", err)
}
return nil
}
func balance(ctx context.Context, rep *Report, sids []string) error {
var g errgroup.Group
for _, sid := range sids {
sid := sid
g.Go(func() error {
body, err := getJSON(ctx, http.DefaultClient, jetURL("account/details"), sid)
if err != nil {
return fmt.Errorf("%w for account/details (3): %w", ErrGetJSON, err)
}
reserved, free, err2 := extractBalance(body)
if err2 != nil {
return fmt.Errorf("couldn't extract balance for account/details (3): %w", err2)
}
rep.Mu.Lock()
rep.Reserved += reserved
rep.Free += free
rep.Mu.Unlock()
return nil
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf("get error for balance: %s", err)
}
return nil
}
func requests(ctx context.Context, rep *Report, sid string) error {
body, err := getJSON(ctx, http.DefaultClient, jetURL("requests/waiting"), sid)
if err != nil {
return fmt.Errorf("%w for requests/waiting: %w", ErrGetJSON, err)
}
requests, err2 := extractRequests(body)
if err2 != nil {
return fmt.Errorf("couldn't extract requests for requests/waiting: %w", err2)
}
rep.Mu.Lock()
rep.Requests = requests
rep.Mu.Unlock()
// fmt.Printf("%+v", requests)
return nil
}
func secondary(ctx context.Context, rep *Report, sid string) error {
all := make([]Secondary, 0)
offset := 0
limit := 100
for {
uri := fmt.Sprintf("exchange/loans?limit=%d&offset=%d&sort_dir=desc&sort_field=ytm", limit, offset)
slog.Info(uri)
body, err := getJSON(ctx, http.DefaultClient, jetURL(
uri,
), sid)
if err != nil {
return fmt.Errorf("%w for exchange/loans: %w", ErrGetJSON, err)
}
// fmt.Printf("body: %s", string(body))
secondary, total, err2 := extractSecondary(body)
if err2 != nil {
return fmt.Errorf("couldn't extract requests for exchange/loans: %w", err2)
}
all = append(all, secondary...)
if offset+limit > total || secondary[0].YTM < 0.18 {
break
}
offset += limit
}
rep.Mu.Lock()
rep.Secondary = all
rep.Mu.Unlock()
return nil
}
// Run make stats
func Run(ctx context.Context, sids []string, terminal, cli bool) (string, error) {
slog.Info("Start")
var (
rep Report
g errgroup.Group
)
rep.Values = make(map[string]float64, 0)
g.Go(func() error {
return loans(ctx, &rep, sids)
})
g.Go(func() error {
return delayed(ctx, &rep, sids)
})
g.Go(func() error {
return balance(ctx, &rep, sids)
})
g.Go(func() error {
sid := sids[0]
if len(sids) > 1 {
sid = sids[1]
}
return requests(ctx, &rep, sid)
})
// g.Go(func() error {
// sid := sids[0]
// if len(sids) > 1 {
// sid = sids[1]
// }
// return secondary(ctx, &rep, sid)
// })
if err := g.Wait(); err != nil {
return "", err
}
return pr(&rep, terminal, cli), nil
}
// WhatBuy every hour
func WhatBuy(ctx context.Context, sids []string, terminal, cli bool) (string, bool, error) {
slog.Info("Start what to buy")
var (
rep Report
g errgroup.Group
)
rep.Values = make(map[string]float64, 0)
g.Go(func() error {
return loans(ctx, &rep, sids)
})
g.Go(func() error {
sid := sids[0]
if len(sids) > 1 {
sid = sids[1]
}
return requests(ctx, &rep, sid)
})
if err := g.Wait(); err != nil {
return "", false, err
}
s, have := needAction(&rep, terminal, cli)
return s, have, nil
}
// SecondaryMarket
func SecondaryMarket(ctx context.Context, sids []string, terminal, cli bool) (string, bool, error) {
slog.Info("Start what to buy")
var (
rep Report
g errgroup.Group
)
rep.Values = make(map[string]float64, 0)
g.Go(func() error {
return loans(ctx, &rep, sids)
})
// g.Go(func() error {
// sid := sids[0]
// if len(sids) > 1 {
// sid = sids[1]
// }
// return requests(ctx, &rep, sid)
// })
g.Go(func() error {
sid := sids[0]
if len(sids) > 1 {
sid = sids[1]
}
return secondary(ctx, &rep, sid)
})
if err := g.Wait(); err != nil {
return "", false, err
}
s, have := needSecondary(&rep, terminal, cli)
return s, have, nil
}
func getJSON(ctx context.Context, client *http.Client, url, sid string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return []byte{}, fmt.Errorf("couldn't create request for url %s: %w", url, err)
}
req.Header.Set("Cookie", "sessionid="+sid)
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0")
resp, err := client.Do(req)
if err != nil {
return []byte{}, fmt.Errorf("couldn't do request for url %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != okCode {
b, _ := io.ReadAll(resp.Body)
return []byte{}, fmt.Errorf("response code %d: %s", resp.StatusCode, string(b))
}
return io.ReadAll(resp.Body)
}
func jetURL(uri string) string {
return "https://jetlend.ru/invest/api/" + uri
}
package pkg
import (
"encoding/json"
"errors"
"sync"
)
type Distr struct {
Data []Data `json:"data"`
}
type Data struct {
Debt float64 `json:"principal_debt"`
LoadID uint `json:"loan_id"`
Company string `json:"company"`
}
type Analytics struct {
Data DataAn `json:"data"`
}
type DataAn struct {
Status Status `json:"status"`
}
type Status struct {
Delayed float64 `json:"delayed"`
}
type Details struct {
Data DataDet `json:"data"`
}
type DataDet struct {
Balance Balance `json:"balance"`
}
type Balance struct {
Reserved float64 `json:"reserved"`
Free float64 `json:"free"`
}
type Report struct {
Sum float64
Values map[string]float64
Delayed float64
Reserved float64
Free float64
Requests []Request
Secondary []Secondary
Mu sync.Mutex
}
func (r *Report) WithMutex(f func(r *Report)) {
r.Mu.Lock()
f(r)
r.Mu.Unlock()
}
type Expect struct {
Data []ExpectData `json:"data"`
}
type ExpectData struct {
Date int64 `json:"date"`
Amount float64 `json:"amount"`
}
type Waiting struct {
Requests []Request `json:"requests"`
}
type Request struct {
Amount CustomFloat64 `json:"amount"`
InterestRate float64 `json:"interest_rate"`
CollectedPercentage float64 `json:"collected_percentage"`
InvestingAmount CustomFloat64 `json:"investing_amount"`
Rating string `json:"rating"`
LoanName string `json:"loan_name"`
Company string `json:"company"`
Term int `json:"term"`
}
type Secondaries struct {
Data []Secondary `json:"data"`
Total int `json:"total"`
}
type Secondary struct {
Company string `json:"company"`
LoanName string `json:"loan_name"`
Term int `json:"term"`
TermLeft int `json:"term_left"`
InterestRate float64 `json:"interest_rate"`
YTM float64 `json:"ytm"`
Rating string `json:"rating"`
Progress float64 `json:"progress"`
PrincipalDebt float64 `json:"principal_debt"`
InvestedCompanyDebt CustomFloat64 `json:"invested_company_debt"`
MinPrice float64 `json:"min_price"`
FinancialDiscipline float64 `json:"financial_discipline"`
Status string `json:"status"`
// Amount CustomFloat64 `json:"amount"`
}
type CustomFloat64 struct {
Float64 float64
}
func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
if data[0] == 34 {
err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
if err != nil {
return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
}
} else {
err := json.Unmarshal(data, &cf.Float64)
if err != nil {
return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
}
}
return nil
}
func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
json, err := json.Marshal(cf.Float64)
return json, err
}