This commit is contained in:
2026-07-21 05:29:37 +03:00
commit edb875f197
299 changed files with 140638 additions and 0 deletions

95
model/db.sql Executable file
View File

@@ -0,0 +1,95 @@
CREATE TABLE tips (
tipID INT NOT NULL AUTO_INCREMENT,
sportID INT NOT NULL,
strategyID INT NOT NULL,
matchID VARCHAR(255) NOT NULL, -- flashscore
tipTime INT NOT NULL,
champID VARCHAR(255) NOT NULL,
champName VARCHAR(255) NOT NULL,
matchName VARCHAR(255) NOT NULL,
market INT NOT NULL,
side INT NOT NULL,
param VARCHAR(255) NOT NULL,
price DOUBLE NOT NULL,
currentMatchTime INT NOT NULL,
notes TEXT NOT NULL, -- formatted json
result VARCHAR(255) NOT NULL,
status INT NOT NULL, -- won, lost, void
onexbetStatsMatchID VARCHAR(255) NOT NULL, -- for inplay tips only
isRated BOOL NOT NULL,
isTeamsSwapped BOOL NOT NULL,
PRIMARY KEY(tipID)
)
ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE markets (
market INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
PRIMARY KEY(marketID)
)
ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE sides (
side INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
PRIMARY KEY(side)
)
ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE sports (
sportID INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
PRIMARY KEY(sportID)
)
ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
CREATE TABLE strategies (
strategyID INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
sportID INT NOT NULL,
notes TEXT NOT NULL,
PRIMARY KEY(strategyID)
)
ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;
--ALTER TABLE tips ADD COLUMN champID VARCHAR(255) NOT NULL;
--ALTER TABLE tips ADD COLUMN isRated BOOL NOT NULL DEFAULT false;
INSERT INTO sports VALUES (1, 'Football');
INSERT INTO sports VALUES (2, 'Handball');
INSERT INTO sports VALUES (3, 'Tennis');
INSERT INTO sports VALUES (4, 'Hockey');
INSERT INTO sports VALUES (5, 'Basketball');
INSERT INTO strategies VALUES (1, 'H1 Over 0.5', 1, '');
INSERT INTO strategies VALUES (2, 'FT Over 0.5', 1, '');
INSERT INTO strategies VALUES (3, 'FT Over 1.5', 1, '');
INSERT INTO strategies VALUES (6, 'H1 Over 0.5 M', 1, '');
INSERT INTO strategies VALUES (7, 'FT Over 0.5 M', 1, '');
INSERT INTO strategies VALUES (8, 'FT Over 1.5 M', 1, '');
INSERT INTO strategies VALUES (4, 'FT Over (-8, 5 min)', 2, '');
INSERT INTO strategies VALUES (5, 'FT Under (+8, 5 min)', 2, '');
INSERT INTO strategies VALUES (9, 'FT Over (-6, 10 min)', 2, '');
INSERT INTO strategies VALUES (10, 'FT Under (+6, 10 min)', 2, '');
INSERT INTO strategies VALUES (11, 'P1 Over (-4, 5 min)', 2, '');
INSERT INTO strategies VALUES (12, 'P1 Under (+4, 5 min)', 2, '');
INSERT INTO strategies VALUES (13, 'P1 Over (-5, 5 min)', 2, '');
INSERT INTO strategies VALUES (14, 'P1 Under (+5, 5 min)', 2, '');
ALTER TABLE tips ADD COLUMN isTeamsSwapped BOOL NOT NULL;

17
model/errors.go Executable file
View File

@@ -0,0 +1,17 @@
package model
import "gordenko.dev/dima/fixme"
const (
EmptyValue = "100"
WrongValue = "101"
Duplicate = "104"
)
var ErrorMessages = map[fixme.Code]string{
EmptyValue: "Пустое значение недопустимо",
WrongValue: "Неверное значение",
Duplicate: "Дубликат!",
}
var fix = fixme.New(ErrorMessages)

469
model/model.go Executable file
View File

@@ -0,0 +1,469 @@
package model
import (
"fmt"
"math"
"sort"
"strings"
"gordenko.dev/dima/qx"
)
func New(qxdb *qx.Db) *Model {
if qxdb == nil {
panic("Param is required")
}
s := new(Model)
s.qx = qxdb
return s
}
type Model struct {
qx *qx.Db
}
func (s *Model) AddTip(tip Tip) (tipID int64, err error) {
/*
if req.Name == "" {
err = api.Err{
Code: EmptyValue,
Field: "name",
}
return
}
*/
tx, err := s.qx.Tx()
if err != nil {
return
}
defer tx.Close(&err)
tipID, err = tx.Insert(tip)
if err != nil {
return
}
err = tx.Commit()
return
}
func (s *Model) GetTip(tipID int64) (_ *Tip, err error) {
tip := Tip{
TipID: tipID,
}
found, err := s.qx.One(&tip)
if err != nil {
return
}
if found {
return &tip, nil
}
return
}
func (s *Model) HasTip(matchID string, strategyID int64) (has bool, err error) {
has, err = s.qx.Has("tips", "WHERE matchID=? AND strategyID=?", matchID, strategyID)
return
}
func (s *Model) ListSportTips(sportID int) (tips []Tip, err error) {
err = s.qx.ListBy(&tips, "WHERE sportID=? ORDER BY tipTime DESC", sportID)
return
}
func (s *Model) ListTips() (tips []Tip, err error) {
err = s.qx.ListBy(&tips, "ORDER BY tipTime DESC")
return
}
func (s *Model) ListWaitingTips() (tips []Tip, err error) {
err = s.qx.ListBy(&tips, "WHERE status=? ORDER BY tipTime DESC", Waiting)
return
}
func (s *Model) ListStrategyTips(strategyID int) (tips []Tip, err error) {
err = s.qx.ListBy(&tips, "WHERE strategyID=? ORDER BY tipTime DESC", strategyID)
return
}
// ListMatchTips - возвращает прогнозы с неизвестным результатом
func (s *Model) ListMatchTips(matchID string) (tips []Tip, err error) {
err = s.qx.ListBy(&tips, "WHERE matchID=? AND status=0", matchID)
return
}
func (s *Model) SetTipResult(req SetTipResultReq) (err error) {
switch req.Status {
case Waiting, Won, Lost, Void:
// pass
default:
err = fix.Field(WrongValue, "status")
return
}
req.Result = strings.TrimSpace(req.Result)
if req.Result == "" {
err = fix.Field(EmptyValue, "result")
return
}
err = s.qx.Exec(`UPDATE tips SET result=?, status=?, notes=? WHERE tipID=?`,
req.Result, req.Status, req.Notes, req.TipID)
return
}
type StrategyPoint struct {
Date int64 `json:"date"`
Profit float64 `json:"profit"`
}
func (s *Model) GetStrategyHistory(strategyID int64) (list []StrategyPoint, err error) {
var (
tips []Tip
profit float64
)
err = s.qx.ListBy(&tips, "WHERE strategyID=? AND status IN (1, 2) ORDER BY tipTime ASC",
strategyID)
if err != nil {
return
}
for _, tip := range tips {
if tip.Status == Won {
profit += (tip.Price - 1) * 10
} else {
profit -= 10
}
list = append(list, StrategyPoint{
Date: tip.TipTime,
Profit: profit,
})
}
return
}
func (s *Model) GetStrategyRatedHistory(strategyID int64) (list []StrategyPoint, err error) {
var (
tips []Tip
profit float64
)
err = s.qx.ListBy(&tips, "WHERE strategyID=? AND status IN (1, 2) AND isRated=true ORDER BY tipTime ASC",
strategyID)
if err != nil {
return
}
for _, tip := range tips {
if tip.Status == Won {
profit += (tip.Price - 1) * 10
} else {
profit -= 10
}
list = append(list, StrategyPoint{
Date: tip.TipTime,
Profit: profit,
})
}
return
}
func (s *Model) ListStrategiesReports(sportID int) (list []StrategyReport, err error) {
var (
tips []Tip
reports = make(map[int64]*StrategyReport)
)
err = s.qx.ListBy(&tips, "WHERE sportID=? AND status IN (1, 2) ORDER BY tipTime ASC", sportID)
if err != nil {
return
}
for _, tip := range tips {
report, ok := reports[tip.StrategyID]
if !ok {
report = &StrategyReport{
StrategyID: tip.StrategyID,
}
reports[tip.StrategyID] = report
}
report.Turnover += 10
if tip.Status == Won {
report.Profit += (tip.Price - 1) * 10
report.WonTips++
} else {
report.Profit -= 10
report.LostTips++
}
if report.Profit < report.Drawdown {
report.Drawdown = report.Profit
}
}
var strategies []Strategy
err = s.qx.ListQuery(&strategies,
"SELECT strategyID, name FROM strategies WHERE sportID=?", sportID)
if err != nil {
return
}
for _, strategy := range strategies {
report, ok := reports[strategy.StrategyID]
if ok {
report.Strategy = strategy.Name
}
}
for _, report := range reports {
// turnover = 100%
// profit = x (ROI)
report.ROI = (report.Profit * 100) / report.Turnover
// won + lost = 100%
// won = x (Accuracy)
report.Accuracy = (float64(report.WonTips) * 100) / float64(report.WonTips+report.LostTips)
list = append(list, *report)
}
sort.Slice(list, func(i, j int) bool {
return list[i].StrategyID < list[j].StrategyID
})
return
}
func (s *Model) GetSportRatedReport(sportID int64) (report SportReport, err error) {
var tips []Tip
report.SportID = sportID
err = s.qx.ListBy(&tips, "WHERE sportID=? AND status IN (1, 2) AND isRated=true ORDER BY tipTime ASC", sportID)
if err != nil {
return
}
for _, tip := range tips {
report.Turnover += 10
if tip.Status == Won {
report.Profit += (tip.Price - 1) * 10
report.WonTips++
} else {
report.Profit -= 10
report.LostTips++
}
if report.Profit < report.Drawdown {
report.Drawdown = report.Profit
}
}
// turnover = 100%
// profit = x (ROI)
report.ROI = (report.Profit * 100) / report.Turnover
// won + lost = 100%
// won = x (Accuracy)
report.Accuracy = (float64(report.WonTips) * 100) / float64(report.WonTips+report.LostTips)
return
}
func getAccuracy(won, lost int) (accuracy float64) {
// won + lost = 100%
// won = x%
// x = (won*100) / (won+lost)
total := won + lost
if total == 0 {
return
}
return float64(won*100) / float64(total)
}
type StrategyChampStats struct {
//StrategyID int64 `json:"strategyID"`
ZoneID string `json:"zoneID" qx:"table=tips"`
ZoneName string `json:"zoneName"`
ChampID string `json:"champID"`
ChampName string `json:"champName"`
Won int `json:"won"`
Lost int `json:"lost"`
AvgPrice float64 `json:"avgPrice"`
}
// Статистика по лигам
func (s *Model) ListStrategyChampStats(strategyID int64) (list []StrategyChampStats, err error) {
err = s.qx.ListQuery(&list, `
SELECT z.zoneID, z.name, t.champID, c.name, COUNT(IF(t.status=1,1,NULL)) as won,
COUNT(IF(t.status=2,1,NULL)) as lost, AVG(t.price)
FROM tips t
INNER JOIN teamChamps c USING(champID)
INNER JOIN teamZones z USING(zoneID)
WHERE t.strategyID=? AND t.status IN (1,2)
GROUP BY t.champID
ORDER BY c.name ASC`, strategyID)
if err != nil {
return
}
eps := 0.001
sort.Slice(list, func(i, j int) bool {
// Сортируем по точности прогнозов
a := getAccuracy(list[i].Won, list[i].Lost)
b := getAccuracy(list[j].Won, list[j].Lost)
dif := math.Abs(a - b)
if dif < eps {
if list[i].Won == list[j].Won {
if list[i].ZoneName == list[j].ZoneName {
return list[i].ChampName < list[j].ChampName
}
return list[i].ZoneName < list[j].ZoneName
}
return list[i].Won > list[j].Won
}
return a > b
})
return
}
// Статистика по лигам
func (s *Model) ListStrategyRatedChampStats(strategyID int64) (list []StrategyChampStats, err error) {
err = s.qx.ListQuery(&list, `
SELECT z.zoneID, z.name, t.champID, c.name, COUNT(IF(t.status=1,1,NULL)) as won,
COUNT(IF(t.status=2,1,NULL)) as lost, AVG(t.price)
FROM tips t
INNER JOIN teamChamps c USING(champID)
INNER JOIN teamZones z USING(zoneID)
WHERE t.strategyID=? AND t.status IN (1,2) AND t.isRated=true
GROUP BY t.champID
ORDER BY c.name ASC`, strategyID)
if err != nil {
return
}
eps := 0.001
sort.Slice(list, func(i, j int) bool {
// Сортируем по точности прогнозов
a := getAccuracy(list[i].Won, list[i].Lost)
b := getAccuracy(list[j].Won, list[j].Lost)
dif := math.Abs(a - b)
if dif < eps {
if list[i].Won == list[j].Won {
if list[i].ZoneName == list[j].ZoneName {
return list[i].ChampName < list[j].ChampName
}
return list[i].ZoneName < list[j].ZoneName
}
return list[i].Won > list[j].Won
}
return a > b
})
return
}
type Strategy struct {
StrategyID int64 `json:"strategyID" qx:"table=strategies; pk"`
Name string `json:"name"`
}
// Статистика по лигам
func (s *Model) ListStrategies() (list []Strategy, err error) {
tmp := []struct {
StrategyID int64
Name string
SportID int64
SportName string
}{}
err = s.qx.ListQuery(&tmp, `
SELECT s.strategyID, s.name, x.sportID, x.name
FROM strategies s
INNER JOIN sports x USING(sportID)
ORDER BY x.sportID ASC, s.strategyID ASC`)
if err != nil {
return
}
for _, x := range tmp {
list = append(list, Strategy{
StrategyID: x.StrategyID,
Name: fmt.Sprintf("#%d %s. %s", x.StrategyID, x.SportName, x.Name),
})
}
return
/*
return []Strategy{
{
StrategyID: 1,
Name: "#1 Алгоритм 1 (Футбол, гол в 1м тайме)",
},
{
StrategyID: 2,
Name: "#2 Алгоритм 2 (Футбол, гол в матче)",
},
{
StrategyID: 3,
Name: "#3 Алгоритм 3 (Футбол, 2й гол в матче)",
},
{
StrategyID: 4,
Name: "#4 Алгоритм ТБ (-9) (Гандбол, ТБ в матче)",
},
{
StrategyID: 5,
Name: "#5 Алгоритм ТМ (+8) (Гандбол, ТМ в матче)",
},
{
StrategyID: 6,
Name: "#6 Алгоритм 1 модифицированный (Футбол, гол в 1м тайме)",
},
{
StrategyID: 7,
Name: "#7 Алгоритм 2 модифицированный (Футбол, гол в матче)",
},
{
StrategyID: 8,
Name: "#8 Алгоритм 3 модифицированный (Футбол, 2й гол в матче)",
},
{
StrategyID: 11,
Name: "#11 Алгоритм ТБ (-6) (Гандбол, ТБ в 1м тайме)",
},
{
StrategyID: 12,
Name: "#12 Алгоритм ТМ (+5) (Гандбол, ТМ в 1м тайме)",
},
}, nil
*/
}

29
model/model_test.go Normal file
View File

@@ -0,0 +1,29 @@
package model
import (
"fmt"
"testing"
"gordenko.dev/dima/qx"
_ "github.com/go-sql-driver/mysql"
)
func TestListStrategyCHampStats(t *testing.T) {
db, err := qx.Open("mysql", "user:password@/tipper")
if err != nil {
panic(err)
}
defer db.Close()
m := New(db)
list, err := m.ListStrategyChampStats(1)
if err != nil {
t.Fatal(err)
}
for _, item := range list {
fmt.Printf("%.2f%% %d - %d\n", getAccuracy(item.Won, item.Lost), item.Won, item.Lost)
}
}

209
model/tipper.Dec2.2020.dump Normal file

File diff suppressed because one or more lines are too long

63
model/types.go Executable file
View File

@@ -0,0 +1,63 @@
package model
type TipStatus int
const (
Waiting TipStatus = 0
Won TipStatus = 1
Lost TipStatus = 2
Void TipStatus = 3
)
type SportReport struct {
SportID int64 `json:"sportID"`
WonTips int `json:"wonTips"`
LostTips int `json:"lostTips"`
Profit float64 `json:"profit"` // прибыль / убыток в деньгах
Turnover float64 `json:"turnover"` // оборот, сумма поставленных денег
ROI float64 `json:"roi"` // return on investments
Drawdown float64 `json:"drawdown"` // просадка, в деньгах
Accuracy float64 `json:"accuracy"` // точность прогноза
}
type StrategyReport struct {
StrategyID int64 `json:"strategyID"`
Strategy string `json:"strategy"`
WonTips int `json:"wonTips"`
LostTips int `json:"lostTips"`
Profit float64 `json:"profit"` // прибыль / убыток в деньгах
Turnover float64 `json:"turnover"` // оборот, сумма поставленных денег
ROI float64 `json:"roi"` // return on investments
Drawdown float64 `json:"drawdown"` // просадка, в деньгах
Accuracy float64 `json:"accuracy"` // точность прогноза
}
type SetTipResultReq struct {
TipID int64 `json:"tipID"`
Result string `json:"result"`
Status TipStatus `json:"status"`
Notes string `json:"notes"`
}
type Tip struct {
TipID int64 `json:"tipID" qx:"table=tips; pk; seq=true"`
SportID int64 `json:"sportID"`
StrategyID int64 `json:"strategyID"`
MatchID string `json:"matchID"`
TipTime int64 `json:"tipTime"`
ChampID string `json:"champID"`
ChampName string `json:"champName"`
MatchName string `json:"matchName"`
Market int `json:"market"`
Side int `json:"side"`
Strategy string `json:"strategy" qx:"-"`
Param string `json:"param"`
Price float64 `json:"price"`
CurrentMatchTime int `json:"currentMatchTime"`
Notes string `json:"notes"`
Result string `json:"result"`
Status TipStatus `json:"status"`
OnexbetStatsMatchID string `json:"onexbetStatsMatchID"`
IsRated bool `json:"isRated"`
IsTeamsSwapped bool `json:"isTeamsSwapped"`
}