summaryrefslogtreecommitdiff
path: root/backend-go/internal/core/market.go
blob: 1cc12d53f99d4ef06659d03d7c87076e5eb7cd7a (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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
package core

import (
	"database/sql"
	"fmt"
	"log"
	"sync"
	"time"

	_ "github.com/mattn/go-sqlite3"

	"eve_haul_go/internal/esi"
)

var RegionIDs = []int{
	10000002, // The Forge
	10000016, // Lonetrek
	10000030, // Heimatar
	10000033, // The Citadel
	10000032, // Sinq Laison
	10000042, // Metropolis
	10000064, // Essence
	10000037, // Everyshore
	10000043, // Domain
	10000036, // Devoid
	10000052, // Kador
	10000067, // Genesis
	10000068, // Verge Vendor
	10000001, // Derelik
	10000020, // Tash-Murkon
}

type Order struct {
	OrderID         int       `json:"order_id"`
	RegionID        int       `json:"region_id"`
	LocationID      int       `json:"location_id"`
	TypeID          int       `json:"type_id"`
	IsBuyOrder      bool      `json:"is_buy_order"`
	Price           float64   `json:"price"`
	Issued          time.Time `json:"issued"`
	VolumeRemain    int       `json:"volume_remain"`
	ItemName        string    `json:"item_name"`
	ItemCargoVolume float64   `json:"item_cargo_volume"`
}

type Trade struct {
	FromStation     int     `json:"from_station"`
	ToStation       int     `json:"to_station"`
	FromOrderID     int     `json:"from_order_id"`
	ToOrderID       int     `json:"to_order_id"`
	ItemName        string  `json:"item_name"`
	TypeID          int     `json:"type_id"`
	ItemCargoVolume float64 `json:"item_cargo_volume"`
	FromPrice       float64 `json:"from_price"`
	ToPrice         float64 `json:"to_price"`
	Quantity        int     `json:"quantity"`
	Cargo           float64 `json:"cargo"`
	GrossProfit     float64 `json:"gross_profit"`
	NetProfit       *float64 `json:"net_profit,omitempty"`
}

type Market struct {
	DB           *sql.DB
	ExpiresTimes map[int]time.Time
	Items        map[int]*Item
	EsiClient    *esi.EsiClient
	Mutex        sync.RWMutex
}

func NewMarket(items map[int]*Item, esiClient *esi.EsiClient) (*Market, error) {
	db, err := sql.Open("sqlite3", ":memory:")
	if err != nil {
		return nil, err
	}

	_, err = db.Exec(`
		CREATE TABLE IF NOT EXISTS orders (
			order_id INTEGER PRIMARY KEY,
			region_id INTEGER,
			location_id INTEGER,
			type_id INTEGER,
			is_buy_order BOOLEAN,
			price REAL,
			issued TEXT,
			volume_remain INTEGER,
			item_name TEXT,
			item_cargo_volume REAL
		)
	`)
	if err != nil {
		return nil, err
	}

	_, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_arbitrage ON orders(type_id, is_buy_order, region_id)`)
	if err != nil {
		return nil, err
	}

	return &Market{
		DB:           db,
		ExpiresTimes: make(map[int]time.Time),
		Items:        items,
		EsiClient:    esiClient,
	}, nil
}

func (m *Market) UpdateOrders() error {
	m.Mutex.Lock()
	defer m.Mutex.Unlock()

	var wg sync.WaitGroup
	errChan := make(chan error, len(RegionIDs))
	ordersChan := make(chan []Order, len(RegionIDs))

	for _, regionID := range RegionIDs {
		expiryTime, exists := m.ExpiresTimes[regionID]
		if !exists || time.Now().UTC().After(expiryTime) {
			wg.Add(1)
			go func(rID int) {
				defer wg.Done()
				orders, err := m.downloadOrders(rID)
				if err != nil {
					errChan <- err
					return
				}
				ordersChan <- orders
			}(regionID)
		}
	}

	wg.Wait()
	close(errChan)
	close(ordersChan)

	var allErrors []error
	for err := range errChan {
		allErrors = append(allErrors, err)
	}

	if len(allErrors) > 0 {
		return fmt.Errorf("errors downloading orders: %v", allErrors)
	}

	for regionOrders := range ordersChan {
		if len(regionOrders) == 0 {
			continue
		}
		rID := regionOrders[0].RegionID

		tx, err := m.DB.Begin()
		if err != nil {
			return err
		}

		_, err = tx.Exec("DELETE FROM orders WHERE region_id = ?", rID)
		if err != nil {
			tx.Rollback()
			return err
		}

		stmt, err := tx.Prepare(`INSERT OR REPLACE INTO orders 
			(order_id, region_id, location_id, type_id, is_buy_order, price, issued, volume_remain, item_name, item_cargo_volume) 
			VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
		if err != nil {
			tx.Rollback()
			return err
		}

		for _, o := range regionOrders {
			_, err = stmt.Exec(o.OrderID, o.RegionID, o.LocationID, o.TypeID, o.IsBuyOrder, o.Price, o.Issued.Format(time.RFC3339), o.VolumeRemain, o.ItemName, o.ItemCargoVolume)
			if err != nil {
				stmt.Close()
				tx.Rollback()
				return err
			}
		}
		stmt.Close()
		tx.Commit()
	}

	return nil
}

func (m *Market) downloadOrders(regionID int) ([]Order, error) {
        page := 1
        var rawOrders []map[string]interface{}
        var lastExpiry string

        for {
                pageOrders, expiry, totalPages, err := m.EsiClient.GetRegionOrders(regionID, page)
                if err != nil {
                        log.Printf("Failed to get region orders %d page %d: %v", regionID, page, err)
                        break
                }
                if len(pageOrders) == 0 {
                        break
                }
                rawOrders = append(rawOrders, pageOrders...)
                lastExpiry = expiry
                
                if page >= totalPages {
                        break
                }
                page++
        }

        if lastExpiry != "" {
                if parsed, err := time.Parse(time.RFC1123, lastExpiry); err == nil {
                        m.ExpiresTimes[regionID] = parsed
                } else {
                        m.ExpiresTimes[regionID] = time.Now().UTC()
                }
        } else {
                m.ExpiresTimes[regionID] = time.Now().UTC()
        }

        var formatted []Order
        for _, o := range rawOrders {
                typeID := int(o["type_id"].(float64))
                item, exists := m.Items[typeID]
                if !exists {
                        continue
                }

                cargoVol := item.Volume
                if cargoVol == nil || *cargoVol <= 0 {
                        continue
                }

                issuedStr, _ := o["issued"].(string)
                issuedTime, _ := time.Parse("2006-01-02T15:04:05Z", issuedStr)

                isBuyOrder, _ := o["is_buy_order"].(bool)
                orderIDFloat, _ := o["order_id"].(float64)
                locIDFloat, _ := o["location_id"].(float64)
                price, _ := o["price"].(float64)
                volRemainFloat, _ := o["volume_remain"].(float64)

                formatted = append(formatted, Order{
                        OrderID:         int(orderIDFloat),
                        RegionID:        regionID,
                        LocationID:      int(locIDFloat),
                        TypeID:          typeID,
                        IsBuyOrder:      isBuyOrder,
                        Price:           price,
                        Issued:          issuedTime,
                        VolumeRemain:    int(volRemainFloat),
                        ItemName:        item.TypeName,
                        ItemCargoVolume: *cargoVol,
                })
        }
        return formatted, nil
}

func (m *Market) ApplyOrderVolumeAdjustments(deductions map[int]int) error {
m.Mutex.Lock()
defer m.Mutex.Unlock()

for orderID, volumeDeducted := range deductions {
log.Printf("Deducting %d volume from order %d", volumeDeducted, orderID)
_, err := m.DB.Exec("UPDATE orders SET volume_remain = volume_remain - ? WHERE order_id = ?", volumeDeducted, orderID)
if err != nil {
return err
}
}
return nil
}