tradedangerous 11.5.3__py3-none-any.whl → 12.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tradedangerous might be problematic. Click here for more details.
- tradedangerous/cache.py +567 -395
- tradedangerous/cli.py +2 -2
- tradedangerous/commands/TEMPLATE.py +25 -26
- tradedangerous/commands/__init__.py +8 -16
- tradedangerous/commands/buildcache_cmd.py +40 -10
- tradedangerous/commands/buy_cmd.py +57 -46
- tradedangerous/commands/commandenv.py +0 -2
- tradedangerous/commands/export_cmd.py +78 -50
- tradedangerous/commands/import_cmd.py +67 -31
- tradedangerous/commands/market_cmd.py +52 -19
- tradedangerous/commands/olddata_cmd.py +120 -107
- tradedangerous/commands/rares_cmd.py +122 -110
- tradedangerous/commands/run_cmd.py +118 -66
- tradedangerous/commands/sell_cmd.py +52 -45
- tradedangerous/commands/shipvendor_cmd.py +49 -234
- tradedangerous/commands/station_cmd.py +55 -485
- tradedangerous/commands/update_cmd.py +56 -420
- tradedangerous/csvexport.py +173 -162
- tradedangerous/gui.py +2 -2
- tradedangerous/plugins/eddblink_plug.py +387 -251
- tradedangerous/plugins/spansh_plug.py +2488 -821
- tradedangerous/prices.py +124 -142
- tradedangerous/templates/TradeDangerous.sql +6 -6
- tradedangerous/tradecalc.py +1227 -1109
- tradedangerous/tradedb.py +533 -384
- tradedangerous/tradeenv.py +12 -1
- tradedangerous/version.py +1 -1
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.0.dist-info}/METADATA +6 -4
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.0.dist-info}/RECORD +33 -38
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.0.dist-info}/WHEEL +1 -1
- tradedangerous/commands/update_gui.py +0 -721
- tradedangerous/jsonprices.py +0 -254
- tradedangerous/plugins/edapi_plug.py +0 -1071
- tradedangerous/plugins/journal_plug.py +0 -537
- tradedangerous/plugins/netlog_plug.py +0 -316
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.0.dist-info}/entry_points.txt +0 -0
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.0.dist-info/licenses}/LICENSE +0 -0
- {tradedangerous-11.5.3.dist-info → tradedangerous-12.0.0.dist-info}/top_level.txt +0 -0
tradedangerous/tradecalc.py
CHANGED
|
@@ -1,1109 +1,1227 @@
|
|
|
1
|
-
# --------------------------------------------------------------------
|
|
2
|
-
# Copyright (C) Oliver 'kfsone' Smith 2014 <oliver@kfs.org>:
|
|
3
|
-
# Copyright (C) Bernd 'Gazelle' Gollesch 2016, 2017
|
|
4
|
-
# Copyright (C)
|
|
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
|
-
import
|
|
47
|
-
import
|
|
48
|
-
import
|
|
49
|
-
|
|
50
|
-
import re
|
|
51
|
-
import sys
|
|
52
|
-
import time
|
|
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
|
-
return self.route[
|
|
168
|
-
|
|
169
|
-
@property
|
|
170
|
-
def
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return Route(
|
|
194
|
-
self.route + (dst,),
|
|
195
|
-
self.hops + (hop,),
|
|
196
|
-
self.startCr,
|
|
197
|
-
self.gainCr + hop[1],
|
|
198
|
-
self.jumps + (jumps,),
|
|
199
|
-
self.score + score,
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
def __lt__(self, rhs):
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"
|
|
283
|
-
+colorize("
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
"
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
"
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
details =
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
station.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
return
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
"""
|
|
523
|
-
if not tdenv:
|
|
524
|
-
tdenv = tdb.tdenv
|
|
525
|
-
self.tdb = tdb
|
|
526
|
-
self.tdenv = tdenv
|
|
527
|
-
self.defaultFit = fit or self.simpleFit
|
|
528
|
-
if "BRUTE_FIT" in os.environ:
|
|
529
|
-
self.defaultFit = self.bruteForceFit
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if tdenv.
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
#
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
if not
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1
|
+
# --------------------------------------------------------------------
|
|
2
|
+
# Copyright (C) Oliver 'kfsone' Smith 2014 <oliver@kfs.org>:
|
|
3
|
+
# Copyright (C) Bernd 'Gazelle' Gollesch 2016, 2017
|
|
4
|
+
# Copyright (C) Stefan 'Tromador' Morrell 2025
|
|
5
|
+
# Copyright (C) Jonathan 'eyeonus' Jones 2018 - 2025
|
|
6
|
+
#
|
|
7
|
+
# You are free to use, redistribute, or even print and eat a copy of
|
|
8
|
+
# this software so long as you include this copyright notice.
|
|
9
|
+
# I guarantee there is at least one bug neither of us knew about.
|
|
10
|
+
# --------------------------------------------------------------------
|
|
11
|
+
# TradeDangerous :: Modules :: Profit Calculator
|
|
12
|
+
#
|
|
13
|
+
# This module has been refactored from legacy SQLite raw SQL access
|
|
14
|
+
# to use SQLAlchemy ORM sessions. It retains the same API surface
|
|
15
|
+
# expected by other modules (mimicking legacy behaviour), but
|
|
16
|
+
# now queries ORM models instead of sqlite3 cursors.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
TradeCalc provides a class for calculating trade loads, hops or
|
|
20
|
+
routes, along with some amount of state.
|
|
21
|
+
|
|
22
|
+
The intent was for it to carry a larger amount of state but
|
|
23
|
+
much of that got moved into TradeEnv, so right now TradeCalc
|
|
24
|
+
looks a little odd.
|
|
25
|
+
|
|
26
|
+
Significant Functions:
|
|
27
|
+
|
|
28
|
+
Tradecalc.getBestHops
|
|
29
|
+
Finds the best "next hop"s given a set of routes.
|
|
30
|
+
|
|
31
|
+
Classes:
|
|
32
|
+
|
|
33
|
+
TradeCalc
|
|
34
|
+
Encapsulates the calculation functions and item-trades,
|
|
35
|
+
|
|
36
|
+
Route
|
|
37
|
+
Describes a sequence of trade hops.
|
|
38
|
+
|
|
39
|
+
TradeLoad
|
|
40
|
+
Describe a cargo load to be carried on a hop.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
######################################################################
|
|
44
|
+
# Imports
|
|
45
|
+
|
|
46
|
+
from collections import defaultdict, namedtuple
|
|
47
|
+
import datetime
|
|
48
|
+
import locale
|
|
49
|
+
import os
|
|
50
|
+
import re
|
|
51
|
+
import sys
|
|
52
|
+
import time
|
|
53
|
+
|
|
54
|
+
from sqlalchemy import select
|
|
55
|
+
|
|
56
|
+
from .tradeexcept import TradeException
|
|
57
|
+
|
|
58
|
+
# ORM models (SQLAlchemy)
|
|
59
|
+
from tradedangerous.db.orm_models import StationItem, Station, System, Item
|
|
60
|
+
from tradedangerous.db.utils import parse_ts # replaces legacy strftime('%s', modified)
|
|
61
|
+
|
|
62
|
+
# Legacy-style helpers (these remain expected by other modules)
|
|
63
|
+
from .tradedb import Trade, Destination, describeAge
|
|
64
|
+
|
|
65
|
+
locale.setlocale(locale.LC_ALL, '')
|
|
66
|
+
|
|
67
|
+
######################################################################
|
|
68
|
+
# Exceptions
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class BadTimestampError(TradeException):
|
|
72
|
+
"""
|
|
73
|
+
Raised when a StationItem row has an invalid or unparsable timestamp.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, tdb, stationID, itemID, modified):
|
|
77
|
+
self.station = tdb.stationByID[stationID]
|
|
78
|
+
self.item = tdb.itemByID[itemID]
|
|
79
|
+
self.modified = modified
|
|
80
|
+
|
|
81
|
+
def __str__(self):
|
|
82
|
+
return (
|
|
83
|
+
"Error loading price data from the local db:\n"
|
|
84
|
+
f"{self.station.name()} has a StationItem entry for "
|
|
85
|
+
f"\"{self.item.name()}\" with an invalid modified timestamp: "
|
|
86
|
+
f"'{self.modified}'."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class NoHopsError(TradeException):
|
|
91
|
+
"""Raised when no possible hops can be generated within constraints."""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
######################################################################
|
|
96
|
+
# TradeLoad (namedtuple wrapper)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TradeLoad(namedtuple("TradeLoad", ("items", "gainCr", "costCr", "units"))):
|
|
100
|
+
"""
|
|
101
|
+
Describes the manifest of items to be exchanged in a trade.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
items : list of (item, qty) tuples tracking the load
|
|
105
|
+
gainCr : predicted total gain in credits
|
|
106
|
+
costCr : how much this load was bought for
|
|
107
|
+
units : total number of units across all items
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __bool__(self):
|
|
111
|
+
return self.units > 0
|
|
112
|
+
|
|
113
|
+
def __lt__(self, rhs):
|
|
114
|
+
if self.gainCr < rhs.gainCr:
|
|
115
|
+
return True
|
|
116
|
+
if rhs.gainCr < self.gainCr:
|
|
117
|
+
return False
|
|
118
|
+
if self.units < rhs.units:
|
|
119
|
+
return True
|
|
120
|
+
if rhs.units < self.units:
|
|
121
|
+
return False
|
|
122
|
+
return self.costCr < rhs.costCr
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def gpt(self):
|
|
126
|
+
"""Gain per ton (credits per unit)."""
|
|
127
|
+
return self.gainCr / self.units if self.units else 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# A convenience empty load (used as sentinel in fitting algorithms).
|
|
131
|
+
emptyLoad = TradeLoad((), 0, 0, 0)
|
|
132
|
+
|
|
133
|
+
######################################################################
|
|
134
|
+
# Classes
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Route:
|
|
138
|
+
"""
|
|
139
|
+
Describes a series of hops where a TradeLoad is picked up at
|
|
140
|
+
one station, the player travels via 0 or more hyperspace
|
|
141
|
+
jumps and docks at a second station where they unload.
|
|
142
|
+
|
|
143
|
+
Example:
|
|
144
|
+
10 Algae + 5 Hydrogen at Station A,
|
|
145
|
+
jump to System2, jump to System3,
|
|
146
|
+
dock at Station B, sell everything, buy gold,
|
|
147
|
+
jump to System4 and sell everything at Station X.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
__slots__ = ("route", "hops", "startCr", "gainCr", "jumps", "score")
|
|
151
|
+
|
|
152
|
+
def __init__(self, stations, hops, startCr, gainCr, jumps, score):
|
|
153
|
+
assert stations
|
|
154
|
+
self.route = stations
|
|
155
|
+
self.hops = hops
|
|
156
|
+
self.startCr = startCr
|
|
157
|
+
self.gainCr = gainCr
|
|
158
|
+
self.jumps = jumps
|
|
159
|
+
self.score = score
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def firstStation(self):
|
|
163
|
+
return self.route[0]
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def firstSystem(self):
|
|
167
|
+
return self.route[0].system
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def lastStation(self):
|
|
171
|
+
return self.route[-1]
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def lastSystem(self):
|
|
175
|
+
return self.route[-1].system
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def avggpt(self):
|
|
179
|
+
if self.hops:
|
|
180
|
+
return sum(hop.gpt for hop in self.hops) // len(self.hops)
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def gpt(self):
|
|
185
|
+
if self.hops:
|
|
186
|
+
return (
|
|
187
|
+
sum(hop.gainCr for hop in self.hops)
|
|
188
|
+
// sum(hop.units for hop in self.hops)
|
|
189
|
+
)
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
def plus(self, dst, hop, jumps, score):
|
|
193
|
+
return Route(
|
|
194
|
+
self.route + (dst,),
|
|
195
|
+
self.hops + (hop,),
|
|
196
|
+
self.startCr,
|
|
197
|
+
self.gainCr + hop[1],
|
|
198
|
+
self.jumps + (jumps,),
|
|
199
|
+
self.score + score,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def __lt__(self, rhs):
|
|
203
|
+
if self.score == rhs.score:
|
|
204
|
+
return len(self.jumps) < len(rhs.jumps)
|
|
205
|
+
return self.score > rhs.score
|
|
206
|
+
|
|
207
|
+
def __eq__(self, rhs):
|
|
208
|
+
return self.score == rhs.score and len(self.jumps) == len(rhs.jumps)
|
|
209
|
+
|
|
210
|
+
def text(self, colorize) -> str:
|
|
211
|
+
return "%s -> %s" % (
|
|
212
|
+
colorize("cyan", self.firstStation.name()),
|
|
213
|
+
colorize("blue", self.lastStation.name()),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def detail(self, tdenv):
|
|
217
|
+
"""
|
|
218
|
+
Legacy helper used by run_cmd.render().
|
|
219
|
+
Renders this route using cmdenv/tdenv display settings.
|
|
220
|
+
|
|
221
|
+
Honors TD_NO_COLOR and tdenv.noColor to disable ANSI color codes.
|
|
222
|
+
"""
|
|
223
|
+
import os
|
|
224
|
+
|
|
225
|
+
# TD_NO_COLOR disables color if set to anything truthy (except 0/false/no/off/"")
|
|
226
|
+
env_val = os.getenv("TD_NO_COLOR", "")
|
|
227
|
+
env_no_color = bool(env_val) and env_val.strip().lower() not in ("0", "", "false", "no", "off")
|
|
228
|
+
|
|
229
|
+
no_color = env_no_color or bool(getattr(tdenv, "noColor", False))
|
|
230
|
+
|
|
231
|
+
if no_color:
|
|
232
|
+
def colorize(_c, s):
|
|
233
|
+
return s
|
|
234
|
+
else:
|
|
235
|
+
_cz = getattr(tdenv, "colorize", None)
|
|
236
|
+
if callable(_cz):
|
|
237
|
+
def colorize(c, s):
|
|
238
|
+
return _cz(c, s)
|
|
239
|
+
else:
|
|
240
|
+
def colorize(_c, s):
|
|
241
|
+
return s
|
|
242
|
+
|
|
243
|
+
detail = int(getattr(tdenv, "detail", 0) or 0)
|
|
244
|
+
goalSystem = getattr(tdenv, "goalSystem", None)
|
|
245
|
+
credits = int(getattr(tdenv, "credits", 0) or 0)
|
|
246
|
+
|
|
247
|
+
return self.render(colorize, tdenv, detail=detail, goalSystem=goalSystem, credits=credits)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def render(self, colorize, tdenv, detail=0, goalSystem=None, credits=0):
|
|
251
|
+
"""
|
|
252
|
+
Produce a formatted string representation of this route.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def genSubValues():
|
|
256
|
+
for hop in self.hops:
|
|
257
|
+
for tr, _ in hop[0]:
|
|
258
|
+
yield len(tr.name(detail))
|
|
259
|
+
|
|
260
|
+
longestNameLen = max(genSubValues(), default=0)
|
|
261
|
+
|
|
262
|
+
text = self.text(colorize)
|
|
263
|
+
if detail >= 1:
|
|
264
|
+
text += f" (score: {self.score:f})"
|
|
265
|
+
text += "\n"
|
|
266
|
+
|
|
267
|
+
jumpsFmt = " Jump {jumps}\n"
|
|
268
|
+
cruiseFmt = " Supercruise to {stn}\n"
|
|
269
|
+
distFmt = None
|
|
270
|
+
|
|
271
|
+
if detail > 1:
|
|
272
|
+
if detail > 2:
|
|
273
|
+
text += self.summary() + "\n"
|
|
274
|
+
if tdenv.maxJumpsPer > 1:
|
|
275
|
+
distFmt = " Direct: {dist:0.2f}ly, Trip: {trav:0.2f}ly\n"
|
|
276
|
+
|
|
277
|
+
hopFmt = (
|
|
278
|
+
" Load from " + colorize("cyan", "{station}") + ":\n{purchases}"
|
|
279
|
+
)
|
|
280
|
+
hopStepFmt = (
|
|
281
|
+
colorize("lightYellow", " {qty:>4}")
|
|
282
|
+
+ " x "
|
|
283
|
+
+ colorize("yellow", "{item:<{longestName}} ")
|
|
284
|
+
+ "{eacost:>8n}cr vs {easell:>8n}cr, "
|
|
285
|
+
"{age}"
|
|
286
|
+
)
|
|
287
|
+
if detail > 2:
|
|
288
|
+
hopStepFmt += ", total: {ttlcost:>10n}cr"
|
|
289
|
+
hopStepFmt += "\n"
|
|
290
|
+
|
|
291
|
+
if not tdenv.summary:
|
|
292
|
+
dockFmt = (
|
|
293
|
+
" Unload at "
|
|
294
|
+
+ colorize("lightBlue", "{station}")
|
|
295
|
+
+ " => Gain {gain:n}cr "
|
|
296
|
+
"({tongain:n}cr/ton) => {credits:n}cr\n"
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
jumpsFmt = re.sub(" ", " ", jumpsFmt, re.M)
|
|
300
|
+
cruiseFmt = re.sub(" ", " ", cruiseFmt, re.M)
|
|
301
|
+
if distFmt:
|
|
302
|
+
distFmt = re.sub(" ", " ", distFmt, re.M)
|
|
303
|
+
hopFmt = "\n" + hopFmt
|
|
304
|
+
dockFmt = " Expect to gain {gain:n}cr ({tongain:n}cr/ton)\n"
|
|
305
|
+
|
|
306
|
+
footer = " " + "-" * 76 + "\n"
|
|
307
|
+
endFmt = (
|
|
308
|
+
"Finish at "
|
|
309
|
+
+ colorize("blue", "{station} ")
|
|
310
|
+
+ "gaining {gain:n}cr ({tongain:n}cr/ton) "
|
|
311
|
+
"=> est {credits:n}cr total\n"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
elif detail:
|
|
315
|
+
hopFmt = " Load from " + colorize("cyan", "{station}") + ":{purchases}\n"
|
|
316
|
+
hopStepFmt = (
|
|
317
|
+
colorize("lightYellow", " {qty}")
|
|
318
|
+
+ " x "
|
|
319
|
+
+ colorize("yellow", "{item}")
|
|
320
|
+
+ " (@{eacost}cr),"
|
|
321
|
+
)
|
|
322
|
+
footer = None
|
|
323
|
+
dockFmt = " Dock at " + colorize("lightBlue", "{station}\n")
|
|
324
|
+
endFmt = (
|
|
325
|
+
" Finish "
|
|
326
|
+
+ colorize("blue", "{station} ")
|
|
327
|
+
+ "+ {gain:n}cr ({tongain:n}cr/ton)"
|
|
328
|
+
"=> {credits:n}cr\n"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
else:
|
|
332
|
+
hopFmt = colorize("cyan", " {station}:{purchases}\n")
|
|
333
|
+
hopStepFmt = (
|
|
334
|
+
colorize("lightYellow", " {qty}")
|
|
335
|
+
+ " x "
|
|
336
|
+
+ colorize("yellow", "{item}")
|
|
337
|
+
+ ","
|
|
338
|
+
)
|
|
339
|
+
footer = None
|
|
340
|
+
dockFmt = None
|
|
341
|
+
endFmt = colorize("blue", " {station}") + " +{gain:n}cr ({tongain:n}/ton)"
|
|
342
|
+
|
|
343
|
+
def jumpList(jumps):
|
|
344
|
+
text, last = "", None
|
|
345
|
+
travelled = 0.0
|
|
346
|
+
for jump in jumps:
|
|
347
|
+
if last:
|
|
348
|
+
dist = last.distanceTo(jump)
|
|
349
|
+
if dist:
|
|
350
|
+
if tdenv.detail:
|
|
351
|
+
text += f", {dist:.2f}ly -> "
|
|
352
|
+
else:
|
|
353
|
+
text += " -> "
|
|
354
|
+
else:
|
|
355
|
+
text += " >>> "
|
|
356
|
+
travelled += dist
|
|
357
|
+
text += jump.name()
|
|
358
|
+
last = jump
|
|
359
|
+
return travelled, text
|
|
360
|
+
|
|
361
|
+
if detail > 1:
|
|
362
|
+
|
|
363
|
+
def decorateStation(station):
|
|
364
|
+
details = []
|
|
365
|
+
if station.lsFromStar:
|
|
366
|
+
details.append(station.distFromStar(True))
|
|
367
|
+
if station.blackMarket != "?":
|
|
368
|
+
details.append("BMk:" + station.blackMarket)
|
|
369
|
+
if station.maxPadSize != "?":
|
|
370
|
+
details.append("Pad:" + station.maxPadSize)
|
|
371
|
+
if station.planetary != "?":
|
|
372
|
+
details.append("Plt:" + station.planetary)
|
|
373
|
+
if station.fleet != "?":
|
|
374
|
+
details.append("Flc:" + station.fleet)
|
|
375
|
+
if station.odyssey != "?":
|
|
376
|
+
details.append("Ody:" + station.odyssey)
|
|
377
|
+
if station.shipyard != "?":
|
|
378
|
+
details.append("Shp:" + station.shipyard)
|
|
379
|
+
if station.outfitting != "?":
|
|
380
|
+
details.append("Out:" + station.outfitting)
|
|
381
|
+
if station.refuel != "?":
|
|
382
|
+
details.append("Ref:" + station.refuel)
|
|
383
|
+
details = "{} ({})".format(
|
|
384
|
+
station.name(), ", ".join(details or ["no details"])
|
|
385
|
+
)
|
|
386
|
+
return details
|
|
387
|
+
|
|
388
|
+
else:
|
|
389
|
+
|
|
390
|
+
def decorateStation(station):
|
|
391
|
+
return station.name()
|
|
392
|
+
|
|
393
|
+
if detail and goalSystem:
|
|
394
|
+
|
|
395
|
+
def goalDistance(station):
|
|
396
|
+
return (
|
|
397
|
+
f" [Distance to {goalSystem.name()}: "
|
|
398
|
+
f"{station.system.distanceTo(goalSystem):.2f} ly]\n"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
else:
|
|
402
|
+
|
|
403
|
+
def goalDistance(station):
|
|
404
|
+
return ""
|
|
405
|
+
|
|
406
|
+
gainCr = 0
|
|
407
|
+
for i, hop in enumerate(self.hops):
|
|
408
|
+
hopGainCr, hopTonnes = hop[1], 0
|
|
409
|
+
purchases = ""
|
|
410
|
+
for (trade, qty) in sorted(
|
|
411
|
+
hop[0],
|
|
412
|
+
key=lambda tradeOpt: tradeOpt[1] * tradeOpt[0].gainCr,
|
|
413
|
+
reverse=True,
|
|
414
|
+
):
|
|
415
|
+
if abs(trade.srcAge - trade.dstAge) <= (30 * 60):
|
|
416
|
+
age = max(trade.srcAge, trade.dstAge)
|
|
417
|
+
age = describeAge(age)
|
|
418
|
+
else:
|
|
419
|
+
srcAge = describeAge(trade.srcAge)
|
|
420
|
+
dstAge = describeAge(trade.dstAge)
|
|
421
|
+
age = f"{srcAge} vs {dstAge}"
|
|
422
|
+
|
|
423
|
+
purchases += hopStepFmt.format(
|
|
424
|
+
qty=qty,
|
|
425
|
+
item=trade.name(detail),
|
|
426
|
+
eacost=trade.costCr,
|
|
427
|
+
easell=trade.costCr + trade.gainCr,
|
|
428
|
+
ttlcost=trade.costCr * qty,
|
|
429
|
+
longestName=longestNameLen,
|
|
430
|
+
age=age,
|
|
431
|
+
)
|
|
432
|
+
hopTonnes += qty
|
|
433
|
+
|
|
434
|
+
text += goalDistance(self.route[i])
|
|
435
|
+
text += hopFmt.format(station=decorateStation(self.route[i]), purchases=purchases)
|
|
436
|
+
|
|
437
|
+
if tdenv.showJumps and jumpsFmt and self.jumps[i]:
|
|
438
|
+
startStn = self.route[i]
|
|
439
|
+
endStn = self.route[i + 1]
|
|
440
|
+
if startStn.system is not endStn.system:
|
|
441
|
+
fmt = jumpsFmt
|
|
442
|
+
travelled, jumps = jumpList(self.jumps[i])
|
|
443
|
+
else:
|
|
444
|
+
fmt = cruiseFmt
|
|
445
|
+
travelled, jumps = 0.0, f"{startStn.name()} >>> {endStn.name()}"
|
|
446
|
+
|
|
447
|
+
text += fmt.format(
|
|
448
|
+
jumps=jumps,
|
|
449
|
+
gain=hopGainCr,
|
|
450
|
+
tongain=hopGainCr / hopTonnes,
|
|
451
|
+
credits=credits + gainCr + hopGainCr,
|
|
452
|
+
stn=self.route[i + 1].dbname,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if travelled and distFmt and len(self.jumps[i]) > 2:
|
|
456
|
+
text += distFmt.format(
|
|
457
|
+
dist=startStn.system.distanceTo(endStn.system), trav=travelled
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if dockFmt:
|
|
461
|
+
stn = self.route[i + 1]
|
|
462
|
+
text += dockFmt.format(
|
|
463
|
+
station=decorateStation(stn),
|
|
464
|
+
gain=hopGainCr,
|
|
465
|
+
tongain=hopGainCr / hopTonnes,
|
|
466
|
+
credits=credits + gainCr + hopGainCr,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
gainCr += hopGainCr
|
|
470
|
+
|
|
471
|
+
lastStation = self.lastStation
|
|
472
|
+
if lastStation.system is not goalSystem:
|
|
473
|
+
text += goalDistance(lastStation)
|
|
474
|
+
text += footer or ""
|
|
475
|
+
text += endFmt.format(
|
|
476
|
+
station=decorateStation(lastStation),
|
|
477
|
+
gain=gainCr,
|
|
478
|
+
credits=credits + gainCr,
|
|
479
|
+
tongain=self.gpt,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
return text
|
|
483
|
+
|
|
484
|
+
def summary(self):
|
|
485
|
+
credits, hops, jumps = self.startCr, self.hops, self.jumps
|
|
486
|
+
ttlGainCr = sum(hop[1] for hop in hops)
|
|
487
|
+
numJumps = sum(
|
|
488
|
+
len(hopJumps) - 1 for hopJumps in jumps if hopJumps
|
|
489
|
+
)
|
|
490
|
+
return (
|
|
491
|
+
"Start CR: {start:10n}\n"
|
|
492
|
+
"Hops : {hops:10n}\n"
|
|
493
|
+
"Jumps : {jumps:10n}\n"
|
|
494
|
+
"Gain CR : {gain:10n}\n"
|
|
495
|
+
"Gain/Hop: {hopgain:10n}\n"
|
|
496
|
+
"Final CR: {final:10n}\n".format(
|
|
497
|
+
start=credits,
|
|
498
|
+
hops=len(hops),
|
|
499
|
+
jumps=numJumps,
|
|
500
|
+
gain=ttlGainCr,
|
|
501
|
+
hopgain=ttlGainCr // len(hops),
|
|
502
|
+
final=credits + ttlGainCr,
|
|
503
|
+
)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
class TradeCalc:
|
|
507
|
+
"""
|
|
508
|
+
Container for accessing trade calculations with common properties.
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
def __init__(self, tdb, tdenv=None, fit=None, items=None):
|
|
512
|
+
"""
|
|
513
|
+
Constructs the TradeCalc object and loads sell/buy data.
|
|
514
|
+
|
|
515
|
+
RAM remediation (per brief):
|
|
516
|
+
- Preload via SQLAlchemy Core/Engine tuples (no ORM entities, no Session).
|
|
517
|
+
- Select only the 9 legacy columns needed for the two maps.
|
|
518
|
+
- Apply ONLY age cutoff + optional item filter.
|
|
519
|
+
- No station reachability gating here.
|
|
520
|
+
- Build stationsSelling/Buying with legacy keep rules & tuple shapes.
|
|
521
|
+
- Compute ageS in Python from parse_ts(modified).
|
|
522
|
+
"""
|
|
523
|
+
if not tdenv:
|
|
524
|
+
tdenv = tdb.tdenv
|
|
525
|
+
self.tdb = tdb
|
|
526
|
+
self.tdenv = tdenv
|
|
527
|
+
self.defaultFit = fit or self.simpleFit
|
|
528
|
+
if "BRUTE_FIT" in os.environ:
|
|
529
|
+
self.defaultFit = self.bruteForceFit
|
|
530
|
+
|
|
531
|
+
minSupply = self.tdenv.supply or 0
|
|
532
|
+
minDemand = self.tdenv.demand or 0
|
|
533
|
+
|
|
534
|
+
# ---------- Build optional item filter (avoidItems + specific items) ----------
|
|
535
|
+
itemFilter = None
|
|
536
|
+
if tdenv.avoidItems or items:
|
|
537
|
+
avoidItemIDs = {item.ID for item in tdenv.avoidItems}
|
|
538
|
+
loadItems = items or tdb.itemByID.values()
|
|
539
|
+
loadIDs = []
|
|
540
|
+
for item in loadItems:
|
|
541
|
+
ID = item if isinstance(item, int) else item.ID
|
|
542
|
+
if ID not in avoidItemIDs:
|
|
543
|
+
loadIDs.append(ID)
|
|
544
|
+
if not loadIDs:
|
|
545
|
+
raise TradeException("No items to load.")
|
|
546
|
+
itemFilter = loadIDs
|
|
547
|
+
|
|
548
|
+
# ---------- Maps and counters ----------
|
|
549
|
+
demand = self.stationsBuying = defaultdict(list)
|
|
550
|
+
supply = self.stationsSelling = defaultdict(list)
|
|
551
|
+
dmdCount = supCount = 0
|
|
552
|
+
nowS = int(time.time())
|
|
553
|
+
|
|
554
|
+
# ---------- Progress heartbeat (only with --progress) ----------
|
|
555
|
+
showProgress = bool(getattr(tdenv, "progress", False))
|
|
556
|
+
hb_interval = 0.5
|
|
557
|
+
last_hb = 0.0
|
|
558
|
+
spinner = ("|", "/", "-", "\\")
|
|
559
|
+
spin_i = 0
|
|
560
|
+
rows_seen = 0
|
|
561
|
+
|
|
562
|
+
def heartbeat():
|
|
563
|
+
nonlocal last_hb, spin_i
|
|
564
|
+
if not showProgress:
|
|
565
|
+
return
|
|
566
|
+
now = time.time()
|
|
567
|
+
if (now - last_hb) < hb_interval:
|
|
568
|
+
return
|
|
569
|
+
last_hb = now
|
|
570
|
+
s = spinner[spin_i]
|
|
571
|
+
spin_i = (spin_i + 1) % len(spinner)
|
|
572
|
+
sys.stdout.write(
|
|
573
|
+
f"\r{s} Scanning market data… rows {rows_seen:n} kept: buys {dmdCount:n}, sells {supCount:n}"
|
|
574
|
+
)
|
|
575
|
+
sys.stdout.flush()
|
|
576
|
+
|
|
577
|
+
# ---------- Core/Engine path (NO Session; NO ORM entities) ----------
|
|
578
|
+
columns = (
|
|
579
|
+
"station_id, item_id, "
|
|
580
|
+
"demand_price, demand_units, demand_level, "
|
|
581
|
+
"supply_price, supply_units, supply_level, "
|
|
582
|
+
"modified"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
where_clauses = []
|
|
586
|
+
params = {}
|
|
587
|
+
|
|
588
|
+
if tdenv.maxAge:
|
|
589
|
+
cutoffS = nowS - (tdenv.maxAge * 60 * 60)
|
|
590
|
+
if tdb.engine.dialect.name == "sqlite":
|
|
591
|
+
where_clauses.append("CAST(strftime('%s', modified) AS INTEGER) >= :cutoffS")
|
|
592
|
+
else:
|
|
593
|
+
where_clauses.append("UNIX_TIMESTAMP(modified) >= :cutoffS")
|
|
594
|
+
params["cutoffS"] = cutoffS
|
|
595
|
+
|
|
596
|
+
if itemFilter:
|
|
597
|
+
where_clauses.append("item_id IN :item_ids")
|
|
598
|
+
params["item_ids"] = tuple(itemFilter)
|
|
599
|
+
|
|
600
|
+
sql = f"SELECT {columns} FROM StationItem"
|
|
601
|
+
if where_clauses:
|
|
602
|
+
sql += " WHERE " + " AND ".join(where_clauses)
|
|
603
|
+
|
|
604
|
+
from sqlalchemy import text as _sa_text
|
|
605
|
+
with tdb.engine.connect() as conn:
|
|
606
|
+
result = conn.execute(_sa_text(sql), params)
|
|
607
|
+
|
|
608
|
+
for (
|
|
609
|
+
stnID,
|
|
610
|
+
itmID,
|
|
611
|
+
d_price, d_units, d_level,
|
|
612
|
+
s_price, s_units, s_level,
|
|
613
|
+
modified,
|
|
614
|
+
) in result:
|
|
615
|
+
rows_seen += 1
|
|
616
|
+
# Compute legacy ageS from modified using parse_ts(...)
|
|
617
|
+
mod_dt = parse_ts(modified)
|
|
618
|
+
if not mod_dt:
|
|
619
|
+
# Finish the line before raising.
|
|
620
|
+
if showProgress:
|
|
621
|
+
sys.stdout.write("\n"); sys.stdout.flush()
|
|
622
|
+
raise BadTimestampError(tdb, stnID, itmID, modified)
|
|
623
|
+
ageS = nowS - int(mod_dt.timestamp())
|
|
624
|
+
|
|
625
|
+
# Buying map (demand side)
|
|
626
|
+
if d_price and d_price > 0 and d_units:
|
|
627
|
+
if not minDemand or d_units >= minDemand:
|
|
628
|
+
demand[stnID].append((itmID, d_price, d_units, d_level, ageS))
|
|
629
|
+
dmdCount += 1
|
|
630
|
+
|
|
631
|
+
# Selling map (supply side)
|
|
632
|
+
if s_price and s_price > 0 and s_units:
|
|
633
|
+
if not minSupply or s_units >= minSupply:
|
|
634
|
+
supply[stnID].append((itmID, s_price, s_units, s_level, ageS))
|
|
635
|
+
supCount += 1
|
|
636
|
+
|
|
637
|
+
heartbeat()
|
|
638
|
+
|
|
639
|
+
# Complete heartbeat line neatly.
|
|
640
|
+
if showProgress:
|
|
641
|
+
sys.stdout.write("\n")
|
|
642
|
+
sys.stdout.flush()
|
|
643
|
+
|
|
644
|
+
# --------- One-time station-ID sets for O(1) membership tests ----------
|
|
645
|
+
self._buying_ids = set(self.stationsBuying.keys())
|
|
646
|
+
self._selling_ids = set(self.stationsSelling.keys())
|
|
647
|
+
self.eligible_station_ids = self._buying_ids & self._selling_ids
|
|
648
|
+
|
|
649
|
+
# --------- Tiny caches valid for the lifetime of this TradeCalc ----------
|
|
650
|
+
self._dst_buy_map = {}
|
|
651
|
+
|
|
652
|
+
tdenv.DEBUG1(
|
|
653
|
+
"Preload used Engine/Core (no ORM identity map). Rows kept: buys={}, sells={}",
|
|
654
|
+
dmdCount, supCount,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# ------------------------------------------------------------------
|
|
659
|
+
# Cargo fitting algorithms
|
|
660
|
+
# ------------------------------------------------------------------
|
|
661
|
+
|
|
662
|
+
def bruteForceFit(self, items, credits, capacity, maxUnits): # pylint: disable=redefined-builtin
|
|
663
|
+
"""
|
|
664
|
+
Brute-force generation of all possible combinations of items.
|
|
665
|
+
This is provided to make it easy to validate the results of future
|
|
666
|
+
variants or optimizations of the fit algorithm.
|
|
667
|
+
"""
|
|
668
|
+
|
|
669
|
+
def _fitCombos(offset, cr, cap, level=1):
|
|
670
|
+
if cr <= 0 or cap <= 0:
|
|
671
|
+
return emptyLoad
|
|
672
|
+
while True:
|
|
673
|
+
if offset >= len(items):
|
|
674
|
+
return emptyLoad
|
|
675
|
+
item = items[offset]
|
|
676
|
+
offset += 1
|
|
677
|
+
|
|
678
|
+
itemCost = item.costCr
|
|
679
|
+
maxQty = min(maxUnits, cap, cr // itemCost)
|
|
680
|
+
|
|
681
|
+
if item.supply < maxQty and item.supply > 0:
|
|
682
|
+
maxQty = min(maxQty, item.supply)
|
|
683
|
+
|
|
684
|
+
if maxQty > 0:
|
|
685
|
+
break
|
|
686
|
+
|
|
687
|
+
bestLoad = _fitCombos(offset, cr, cap, level + 1)
|
|
688
|
+
itemGain = item.gainCr
|
|
689
|
+
|
|
690
|
+
for qty in range(1, maxQty + 1):
|
|
691
|
+
loadGain, loadCost = itemGain * qty, itemCost * qty
|
|
692
|
+
load = TradeLoad(((item, qty),), loadGain, loadCost, qty)
|
|
693
|
+
subLoad = _fitCombos(offset, cr - loadCost, cap - qty, level + 1)
|
|
694
|
+
combGain = loadGain + subLoad.gainCr
|
|
695
|
+
if combGain < bestLoad.gainCr:
|
|
696
|
+
continue
|
|
697
|
+
combCost = loadCost + subLoad.costCr
|
|
698
|
+
combUnits = qty + subLoad.units
|
|
699
|
+
if combGain == bestLoad.gainCr:
|
|
700
|
+
if combUnits > bestLoad.units:
|
|
701
|
+
continue
|
|
702
|
+
if combUnits == bestLoad.units:
|
|
703
|
+
if combCost >= bestLoad.costCr:
|
|
704
|
+
continue
|
|
705
|
+
bestLoad = TradeLoad(
|
|
706
|
+
load.items + subLoad.items, combGain, combCost, combUnits
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
return bestLoad
|
|
710
|
+
|
|
711
|
+
return _fitCombos(0, credits, capacity)
|
|
712
|
+
|
|
713
|
+
def fastFit(self, items, credits, capacity, maxUnits): # pylint: disable=redefined-builtin
|
|
714
|
+
"""
|
|
715
|
+
Best load calculator using a recursive knapsack-like
|
|
716
|
+
algorithm to find multiple loads and return the best.
|
|
717
|
+
[eyeonus] Left in for the masochists, as this becomes
|
|
718
|
+
horribly slow at stations with many items for sale.
|
|
719
|
+
As in iooks-like-the-program-has-frozen slow.
|
|
720
|
+
"""
|
|
721
|
+
|
|
722
|
+
def _fitCombos(offset, cr, cap):
|
|
723
|
+
"""
|
|
724
|
+
Starting from offset, consider a scenario where we
|
|
725
|
+
would purchase the maximum number of each item
|
|
726
|
+
given the cr+cap limitations. Then, assuming that
|
|
727
|
+
load, solve for the remaining cr+cap from the next
|
|
728
|
+
value of offset.
|
|
729
|
+
|
|
730
|
+
The "best fit" is not always the most profitable,
|
|
731
|
+
so we yield all the results and leave the caller
|
|
732
|
+
to determine which is actually most profitable.
|
|
733
|
+
"""
|
|
734
|
+
bestGainCr = -1
|
|
735
|
+
bestItem = None
|
|
736
|
+
bestQty = 0
|
|
737
|
+
bestCostCr = 0
|
|
738
|
+
bestSub = None
|
|
739
|
+
|
|
740
|
+
qtyCeil = min(maxUnits, cap)
|
|
741
|
+
|
|
742
|
+
for iNo in range(offset, len(items)):
|
|
743
|
+
item = items[iNo]
|
|
744
|
+
itemCostCr = item.costCr
|
|
745
|
+
maxQty = min(qtyCeil, cr // itemCostCr)
|
|
746
|
+
|
|
747
|
+
if maxQty <= 0:
|
|
748
|
+
continue
|
|
749
|
+
|
|
750
|
+
supply = item.supply
|
|
751
|
+
if supply <= 0:
|
|
752
|
+
continue
|
|
753
|
+
|
|
754
|
+
maxQty = min(maxQty, supply)
|
|
755
|
+
|
|
756
|
+
itemGainCr = item.gainCr
|
|
757
|
+
if maxQty == cap:
|
|
758
|
+
gain = itemGainCr * maxQty
|
|
759
|
+
if gain > bestGainCr:
|
|
760
|
+
cost = itemCostCr * maxQty
|
|
761
|
+
bestGainCr = gain
|
|
762
|
+
bestItem = item
|
|
763
|
+
bestQty = maxQty
|
|
764
|
+
bestCostCr = cost
|
|
765
|
+
bestSub = None
|
|
766
|
+
break
|
|
767
|
+
|
|
768
|
+
loadCostCr = maxQty * itemCostCr
|
|
769
|
+
loadGainCr = maxQty * itemGainCr
|
|
770
|
+
if loadGainCr > bestGainCr:
|
|
771
|
+
bestGainCr = loadGainCr
|
|
772
|
+
bestCostCr = loadCostCr
|
|
773
|
+
bestItem = item
|
|
774
|
+
bestQty = maxQty
|
|
775
|
+
bestSub = None
|
|
776
|
+
|
|
777
|
+
crLeft, capLeft = cr - loadCostCr, cap - maxQty
|
|
778
|
+
if crLeft > 0 and capLeft > 0:
|
|
779
|
+
subLoad = _fitCombos(iNo + 1, crLeft, capLeft)
|
|
780
|
+
if subLoad is emptyLoad:
|
|
781
|
+
continue
|
|
782
|
+
ttlGain = loadGainCr + subLoad.gainCr
|
|
783
|
+
if ttlGain < bestGainCr:
|
|
784
|
+
continue
|
|
785
|
+
ttlCost = loadCostCr + subLoad.costCr
|
|
786
|
+
if ttlGain == bestGainCr and ttlCost >= bestCostCr:
|
|
787
|
+
continue
|
|
788
|
+
bestGainCr = ttlGain
|
|
789
|
+
bestItem = item
|
|
790
|
+
bestQty = maxQty
|
|
791
|
+
bestCostCr = ttlCost
|
|
792
|
+
bestSub = subLoad
|
|
793
|
+
|
|
794
|
+
if not bestItem:
|
|
795
|
+
return emptyLoad
|
|
796
|
+
|
|
797
|
+
bestLoad = ((bestItem, bestQty),)
|
|
798
|
+
if bestSub:
|
|
799
|
+
bestLoad = bestLoad + bestSub.items
|
|
800
|
+
bestQty += bestSub.units
|
|
801
|
+
return TradeLoad(bestLoad, bestGainCr, bestCostCr, bestQty)
|
|
802
|
+
|
|
803
|
+
return _fitCombos(0, credits, capacity)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
# Mark's test run, to spare searching back through the forum posts for it.
|
|
807
|
+
# python trade.py run --fr="Orang/Bessel Gateway" --cap=720 --cr=11b --ly=24.73 --empty=37.61 --pad=L --hops=2 --jum=3 --loop --summary -vv --progress
|
|
808
|
+
|
|
809
|
+
def simpleFit(self, items, credits, capacity, maxUnits): # pylint: disable=redefined-builtin
|
|
810
|
+
"""
|
|
811
|
+
Simplistic load calculator:
|
|
812
|
+
(The item list is sorted with highest profit margin items in front.)
|
|
813
|
+
Step 1: Fill hold with as much of item1 as possible based on the limiting
|
|
814
|
+
factors of hold size, supply amount, and available credits.
|
|
815
|
+
|
|
816
|
+
Step 2: If there is space in the hold and money available, repeat Step 1
|
|
817
|
+
with item2, item3, etc. until either the hold is filled
|
|
818
|
+
or the commander is too poor to buy more.
|
|
819
|
+
|
|
820
|
+
When amount of credits isn't a limiting factor, this should produce
|
|
821
|
+
the most profitable route ~99.7% of the time, and still be very
|
|
822
|
+
close to the most profitable the rest of the time.
|
|
823
|
+
(Very close = not enough less profit that anyone should care,
|
|
824
|
+
especially since this thing doesn't suffer slowdowns like fastFit.)
|
|
825
|
+
"""
|
|
826
|
+
|
|
827
|
+
n = 0
|
|
828
|
+
load = ()
|
|
829
|
+
gainCr = 0
|
|
830
|
+
costCr = 0
|
|
831
|
+
qty = 0
|
|
832
|
+
while n < len(items) and credits > 0 and capacity > 0:
|
|
833
|
+
qtyCeil = min(maxUnits, capacity)
|
|
834
|
+
|
|
835
|
+
item = items[n]
|
|
836
|
+
maxQty = min(qtyCeil, credits // item.costCr)
|
|
837
|
+
|
|
838
|
+
if maxQty > 0 and item.supply > 0:
|
|
839
|
+
maxQty = min(maxQty, item.supply)
|
|
840
|
+
|
|
841
|
+
loadCostCr = maxQty * item.costCr
|
|
842
|
+
loadGainCr = maxQty * item.gainCr
|
|
843
|
+
|
|
844
|
+
load = load + ((item, maxQty),)
|
|
845
|
+
qty += maxQty
|
|
846
|
+
capacity -= maxQty
|
|
847
|
+
|
|
848
|
+
gainCr += loadGainCr
|
|
849
|
+
costCr += loadCostCr
|
|
850
|
+
credits -= loadCostCr
|
|
851
|
+
|
|
852
|
+
n += 1
|
|
853
|
+
|
|
854
|
+
return TradeLoad(load, gainCr, costCr, qty)
|
|
855
|
+
|
|
856
|
+
# ------------------------------------------------------------------
|
|
857
|
+
# Trading methods
|
|
858
|
+
# ------------------------------------------------------------------
|
|
859
|
+
|
|
860
|
+
def getTrades(self, srcStation, dstStation, srcSelling=None):
|
|
861
|
+
"""
|
|
862
|
+
Returns the most profitable trading options from one station to another.
|
|
863
|
+
"""
|
|
864
|
+
if not srcSelling:
|
|
865
|
+
srcSelling = self.stationsSelling.get(srcStation.ID, None)
|
|
866
|
+
if not srcSelling:
|
|
867
|
+
return None
|
|
868
|
+
|
|
869
|
+
dstBuying = self.stationsBuying.get(dstStation.ID, None)
|
|
870
|
+
if not dstBuying:
|
|
871
|
+
return None
|
|
872
|
+
|
|
873
|
+
minGainCr = max(1, self.tdenv.minGainPerTon or 1)
|
|
874
|
+
maxGainCr = max(minGainCr, self.tdenv.maxGainPerTon or sys.maxsize)
|
|
875
|
+
|
|
876
|
+
# ---- per-destination buy map cache (item_id -> buy tuple) ----
|
|
877
|
+
buy_map = self._dst_buy_map.get(dstStation.ID)
|
|
878
|
+
if buy_map is None:
|
|
879
|
+
# list -> dict once, re-used across many src comparisons
|
|
880
|
+
buy_map = {buy[0]: buy for buy in dstBuying}
|
|
881
|
+
self._dst_buy_map[dstStation.ID] = buy_map
|
|
882
|
+
getBuy = buy_map.get
|
|
883
|
+
|
|
884
|
+
itemIdx = self.tdb.itemByID
|
|
885
|
+
trading = []
|
|
886
|
+
append_trade = trading.append
|
|
887
|
+
|
|
888
|
+
for sell in srcSelling:
|
|
889
|
+
buy = getBuy(sell[0])
|
|
890
|
+
if not buy:
|
|
891
|
+
continue
|
|
892
|
+
gainCr = buy[1] - sell[1]
|
|
893
|
+
if minGainCr <= gainCr <= maxGainCr:
|
|
894
|
+
append_trade(
|
|
895
|
+
Trade(
|
|
896
|
+
itemIdx[sell[0]],
|
|
897
|
+
sell[1],
|
|
898
|
+
gainCr,
|
|
899
|
+
sell[2],
|
|
900
|
+
sell[3],
|
|
901
|
+
buy[2],
|
|
902
|
+
buy[3],
|
|
903
|
+
sell[4],
|
|
904
|
+
buy[4],
|
|
905
|
+
)
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
# Same final ordering as two successive sorts:
|
|
909
|
+
# primary: gainCr desc, tiebreak: costCr asc
|
|
910
|
+
trading.sort(key=lambda t: (-t.gainCr, t.costCr))
|
|
911
|
+
|
|
912
|
+
return trading
|
|
913
|
+
|
|
914
|
+
def getBestHops(self, routes, restrictTo=None):
|
|
915
|
+
"""
|
|
916
|
+
Given a list of routes, try all available next hops from each route.
|
|
917
|
+
Keeps only the best candidate per destination station for this hop.
|
|
918
|
+
"""
|
|
919
|
+
|
|
920
|
+
tdb = self.tdb
|
|
921
|
+
tdenv = self.tdenv
|
|
922
|
+
avoidPlaces = getattr(tdenv, "avoidPlaces", None) or ()
|
|
923
|
+
assert not restrictTo or isinstance(restrictTo, set)
|
|
924
|
+
maxJumpsPer = tdenv.maxJumpsPer
|
|
925
|
+
maxLyPer = tdenv.maxLyPer
|
|
926
|
+
maxPadSize = tdenv.padSize
|
|
927
|
+
planetary = tdenv.planetary
|
|
928
|
+
fleet = tdenv.fleet
|
|
929
|
+
odyssey = tdenv.odyssey
|
|
930
|
+
noPlanet = tdenv.noPlanet
|
|
931
|
+
maxLsFromStar = tdenv.maxLs or float("inf")
|
|
932
|
+
reqBlackMarket = getattr(tdenv, "blackMarket", False) or False
|
|
933
|
+
maxAge = getattr(tdenv, "maxAge") or 0
|
|
934
|
+
credits = tdenv.credits - (getattr(tdenv, "insurance", 0) or 0)
|
|
935
|
+
fitFunction = self.defaultFit
|
|
936
|
+
capacity = tdenv.capacity
|
|
937
|
+
maxUnits = getattr(tdenv, "limit") or capacity
|
|
938
|
+
|
|
939
|
+
buying_ids = self._buying_ids
|
|
940
|
+
|
|
941
|
+
bestToDest = {}
|
|
942
|
+
safetyMargin = 1.0 - tdenv.margin
|
|
943
|
+
unique = tdenv.unique
|
|
944
|
+
loopInt = getattr(tdenv, "loopInt", 0) or None
|
|
945
|
+
|
|
946
|
+
if tdenv.lsPenalty:
|
|
947
|
+
lsPenalty = max(min(tdenv.lsPenalty / 100, 1), 0)
|
|
948
|
+
else:
|
|
949
|
+
lsPenalty = 0
|
|
950
|
+
|
|
951
|
+
goalSystem = tdenv.goalSystem
|
|
952
|
+
uniquePath = None
|
|
953
|
+
|
|
954
|
+
restrictStations = set()
|
|
955
|
+
if restrictTo:
|
|
956
|
+
for place in restrictTo:
|
|
957
|
+
if isinstance(place, Station):
|
|
958
|
+
restrictStations.add(place)
|
|
959
|
+
elif isinstance(place, System) and place.stations:
|
|
960
|
+
restrictStations.update(place.stations)
|
|
961
|
+
|
|
962
|
+
# -----------------------
|
|
963
|
+
# Spinner (stderr; only with --progress)
|
|
964
|
+
# -----------------------
|
|
965
|
+
heartbeat_enabled = bool(getattr(tdenv, "progress", False))
|
|
966
|
+
hb_interval = 0.5
|
|
967
|
+
last_hb = 0.0
|
|
968
|
+
spinner = ("|", "/", "-", "\\")
|
|
969
|
+
spin_i = 0
|
|
970
|
+
total_origins = len(routes)
|
|
971
|
+
best_seen_score = -1 # hop-global best hop score, nearest int
|
|
972
|
+
|
|
973
|
+
def heartbeat(origin_idx, dests_checked):
|
|
974
|
+
nonlocal last_hb, spin_i
|
|
975
|
+
if not heartbeat_enabled:
|
|
976
|
+
return
|
|
977
|
+
now = time.time()
|
|
978
|
+
if now - last_hb < hb_interval:
|
|
979
|
+
return
|
|
980
|
+
last_hb = now
|
|
981
|
+
s = spinner[spin_i]
|
|
982
|
+
spin_i = (spin_i + 1) % len(spinner)
|
|
983
|
+
sys.stderr.write(
|
|
984
|
+
f"\r{s} origin {origin_idx}/{total_origins} destinations checked: {dests_checked:n} best score: {max(0, best_seen_score):n}"
|
|
985
|
+
)
|
|
986
|
+
sys.stderr.flush()
|
|
987
|
+
|
|
988
|
+
if tdenv.direct:
|
|
989
|
+
if goalSystem and not restrictTo:
|
|
990
|
+
restrictTo = (goalSystem,)
|
|
991
|
+
restrictStations = set(goalSystem.stations)
|
|
992
|
+
if avoidPlaces:
|
|
993
|
+
restrictStations = set(
|
|
994
|
+
stn
|
|
995
|
+
for stn in restrictStations
|
|
996
|
+
if stn not in avoidPlaces and stn.system not in avoidPlaces
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
def station_iterator(srcStation, origin_idx):
|
|
1000
|
+
srcSys = srcStation.system
|
|
1001
|
+
srcDist = srcSys.distanceTo
|
|
1002
|
+
dests_seen = 0
|
|
1003
|
+
for stn in restrictStations:
|
|
1004
|
+
stnSys = stn.system
|
|
1005
|
+
if stn.ID not in buying_ids:
|
|
1006
|
+
continue
|
|
1007
|
+
dests_seen += 1
|
|
1008
|
+
heartbeat(origin_idx, dests_seen)
|
|
1009
|
+
yield Destination(stnSys, stn, (srcSys, stnSys), srcDist(stnSys))
|
|
1010
|
+
|
|
1011
|
+
else:
|
|
1012
|
+
getDestinations = tdb.getDestinations
|
|
1013
|
+
|
|
1014
|
+
def station_iterator(srcStation, origin_idx):
|
|
1015
|
+
dests_seen = 0
|
|
1016
|
+
for d in getDestinations(
|
|
1017
|
+
srcStation,
|
|
1018
|
+
maxJumps=maxJumpsPer,
|
|
1019
|
+
maxLyPer=maxLyPer,
|
|
1020
|
+
avoidPlaces=avoidPlaces,
|
|
1021
|
+
maxPadSize=maxPadSize,
|
|
1022
|
+
maxLsFromStar=maxLsFromStar,
|
|
1023
|
+
noPlanet=noPlanet,
|
|
1024
|
+
planetary=planetary,
|
|
1025
|
+
fleet=fleet,
|
|
1026
|
+
odyssey=odyssey,
|
|
1027
|
+
):
|
|
1028
|
+
dests_seen += 1
|
|
1029
|
+
heartbeat(origin_idx, dests_seen)
|
|
1030
|
+
if d.station.ID in buying_ids:
|
|
1031
|
+
yield d
|
|
1032
|
+
|
|
1033
|
+
connections = 0
|
|
1034
|
+
getSelling = self.stationsSelling.get
|
|
1035
|
+
|
|
1036
|
+
for route_no, route in enumerate(routes):
|
|
1037
|
+
tdenv.DEBUG1("Route = {}", route.text(lambda x, y: y))
|
|
1038
|
+
|
|
1039
|
+
srcStation = route.lastStation
|
|
1040
|
+
startCr = credits + int(route.gainCr * safetyMargin)
|
|
1041
|
+
|
|
1042
|
+
srcSelling = getSelling(srcStation.ID, None)
|
|
1043
|
+
if not srcSelling:
|
|
1044
|
+
tdenv.DEBUG1("Nothing sold at source - next.")
|
|
1045
|
+
heartbeat(route_no + 1, 0)
|
|
1046
|
+
continue
|
|
1047
|
+
|
|
1048
|
+
srcSelling = tuple(values for values in srcSelling if values[1] <= startCr)
|
|
1049
|
+
if not srcSelling:
|
|
1050
|
+
tdenv.DEBUG1("Nothing affordable - next.")
|
|
1051
|
+
heartbeat(route_no + 1, 0)
|
|
1052
|
+
continue
|
|
1053
|
+
|
|
1054
|
+
if goalSystem:
|
|
1055
|
+
origSystem = route.firstSystem
|
|
1056
|
+
srcSystem = srcStation.system
|
|
1057
|
+
srcDistTo = srcSystem.distanceTo
|
|
1058
|
+
goalDistTo = goalSystem.distanceTo
|
|
1059
|
+
origDistTo = origSystem.distanceTo
|
|
1060
|
+
srcGoalDist = srcDistTo(goalSystem)
|
|
1061
|
+
srcOrigDist = srcDistTo(origSystem)
|
|
1062
|
+
origGoalDist = origDistTo(goalSystem)
|
|
1063
|
+
|
|
1064
|
+
if unique:
|
|
1065
|
+
uniquePath = route.route
|
|
1066
|
+
elif loopInt:
|
|
1067
|
+
pos_from_end = 0 - loopInt
|
|
1068
|
+
uniquePath = route.route[pos_from_end:-1]
|
|
1069
|
+
|
|
1070
|
+
stations = (
|
|
1071
|
+
d
|
|
1072
|
+
for d in station_iterator(srcStation, route_no + 1)
|
|
1073
|
+
if (d.station != srcStation)
|
|
1074
|
+
and (d.station.blackMarket == "Y" if reqBlackMarket else True)
|
|
1075
|
+
and (d.station not in uniquePath if uniquePath else True)
|
|
1076
|
+
and (d.station in restrictStations if restrictStations else True)
|
|
1077
|
+
and (d.station.dataAge and d.station.dataAge <= maxAge if maxAge else True)
|
|
1078
|
+
and (
|
|
1079
|
+
(
|
|
1080
|
+
(d.system is not srcSystem)
|
|
1081
|
+
if bool(tdenv.unique)
|
|
1082
|
+
else (d.system is goalSystem or d.distLy < srcGoalDist)
|
|
1083
|
+
)
|
|
1084
|
+
if goalSystem
|
|
1085
|
+
else True
|
|
1086
|
+
)
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
if tdenv.debug >= 1:
|
|
1090
|
+
def annotate(dest):
|
|
1091
|
+
tdenv.DEBUG1(
|
|
1092
|
+
"destSys {}, destStn {}, jumps {}, distLy {}",
|
|
1093
|
+
dest.system.dbname,
|
|
1094
|
+
dest.station.dbname,
|
|
1095
|
+
"->".join(jump.text() for jump in dest.via),
|
|
1096
|
+
dest.distLy,
|
|
1097
|
+
)
|
|
1098
|
+
return True
|
|
1099
|
+
stations = (d for d in stations if annotate(d))
|
|
1100
|
+
|
|
1101
|
+
for dest in stations:
|
|
1102
|
+
dstStation = dest.station
|
|
1103
|
+
connections += 1
|
|
1104
|
+
|
|
1105
|
+
items = self.getTrades(srcStation, dstStation, srcSelling)
|
|
1106
|
+
if not items:
|
|
1107
|
+
continue
|
|
1108
|
+
trade = fitFunction(items, startCr, capacity, maxUnits)
|
|
1109
|
+
|
|
1110
|
+
multiplier = 1.0
|
|
1111
|
+
# Calculate total K-lightseconds supercruise time.
|
|
1112
|
+
# This will amortize for the start/end stations
|
|
1113
|
+
dstSys = dest.system
|
|
1114
|
+
if goalSystem and dstSys is not goalSystem:
|
|
1115
|
+
# Biggest reward for shortening distance to goal
|
|
1116
|
+
dstGoalDist = goalDistTo(dstSys)
|
|
1117
|
+
# bias towards bigger reductions
|
|
1118
|
+
score = 5000 * origGoalDist / dstGoalDist
|
|
1119
|
+
# discourage moving back towards origin
|
|
1120
|
+
score += 50 * srcGoalDist / dstGoalDist
|
|
1121
|
+
# Gain per unit pays a small part
|
|
1122
|
+
if dstSys is not origSystem:
|
|
1123
|
+
score += 10 * (origDistTo(dstSys) - srcOrigDist)
|
|
1124
|
+
score += (trade.gainCr / trade.units) / 25
|
|
1125
|
+
else:
|
|
1126
|
+
score = trade.gainCr
|
|
1127
|
+
|
|
1128
|
+
if lsPenalty:
|
|
1129
|
+
|
|
1130
|
+
def sigmoid(x):
|
|
1131
|
+
# [eyeonus]:
|
|
1132
|
+
# (Keep in mind all this ignores values of x<0.)
|
|
1133
|
+
# The sigmoid: (1-(25(x-1))/(1+abs(25(x-1))))/4
|
|
1134
|
+
# ranges between 0.5 and 0 with a drop around x=1,
|
|
1135
|
+
# which makes it great for giving a boost to distances < 1Kls.
|
|
1136
|
+
#
|
|
1137
|
+
# The sigmoid: (-1-(50(x-4))/(1+abs(50(x-4))))/4
|
|
1138
|
+
# ranges between 0 and -0.5 with a drop around x=4,
|
|
1139
|
+
# making it great for penalizing distances > 4Kls.
|
|
1140
|
+
#
|
|
1141
|
+
# The curve: (-1+1/(x+1)^((x+1)/4))/2
|
|
1142
|
+
# ranges between 0 and -0.5 in a smooth arc,
|
|
1143
|
+
# which will be used for making distances
|
|
1144
|
+
# closer to 4Kls get a slightly higher penalty
|
|
1145
|
+
# then distances closer to 1Kls.
|
|
1146
|
+
#
|
|
1147
|
+
# Adding the three together creates a doubly-kinked curve
|
|
1148
|
+
# that ranges from ~0.5 to -1.0, with drops around x=1 and x=4,
|
|
1149
|
+
# which closely matches ksfone's intention without going into
|
|
1150
|
+
# negative numbers and causing problems when we add it to
|
|
1151
|
+
# the multiplier variable. ( 1 + -1 = 0 )
|
|
1152
|
+
#
|
|
1153
|
+
# You can see a graph of the formula here:
|
|
1154
|
+
# https://goo.gl/sn1PqQ
|
|
1155
|
+
# NOTE: The black curve is at a penalty of 0%,
|
|
1156
|
+
# the red curve at a penalty of 100%, with intermediates at
|
|
1157
|
+
# 25%, 50%, and 75%.
|
|
1158
|
+
# The other colored lines show the penalty curves individually
|
|
1159
|
+
# and the teal composite of all three.
|
|
1160
|
+
return x / (1 + abs(x))
|
|
1161
|
+
# [kfsone] Only want 1dp
|
|
1162
|
+
# Produce a curve that favors distances under 1kls
|
|
1163
|
+
# positively, starts to penalize distances over 1k,
|
|
1164
|
+
# and after 4kls starts to penalize aggressively
|
|
1165
|
+
# http://goo.gl/Otj2XP
|
|
1166
|
+
|
|
1167
|
+
# [eyeonus] As aadler pointed out, this goes into negative
|
|
1168
|
+
# numbers, which causes problems.
|
|
1169
|
+
# penalty = ((cruiseKls ** 2) - cruiseKls) / 3
|
|
1170
|
+
# penalty *= lsPenalty
|
|
1171
|
+
# multiplier *= (1 - penalty)
|
|
1172
|
+
cruiseKls = int(dstStation.lsFromStar / 100) / 10
|
|
1173
|
+
boost = (1 - sigmoid(25 * (cruiseKls - 1))) / 4
|
|
1174
|
+
drop = (-1 - sigmoid(50 * (cruiseKls - 4))) / 4
|
|
1175
|
+
try:
|
|
1176
|
+
penalty = (-1 + 1 / (cruiseKls + 1) ** ((cruiseKls + 1) / 4)) / 2
|
|
1177
|
+
except OverflowError:
|
|
1178
|
+
penalty = -0.5
|
|
1179
|
+
multiplier += (penalty + boost + drop) * lsPenalty
|
|
1180
|
+
|
|
1181
|
+
score *= multiplier
|
|
1182
|
+
|
|
1183
|
+
# update hop-global best score (nearest int)
|
|
1184
|
+
try:
|
|
1185
|
+
si = int(round(score))
|
|
1186
|
+
except Exception:
|
|
1187
|
+
si = int(score)
|
|
1188
|
+
if si > best_seen_score:
|
|
1189
|
+
best_seen_score = si
|
|
1190
|
+
|
|
1191
|
+
dstID = dstStation.ID
|
|
1192
|
+
try:
|
|
1193
|
+
btd = bestToDest[dstID]
|
|
1194
|
+
except KeyError:
|
|
1195
|
+
pass
|
|
1196
|
+
else:
|
|
1197
|
+
bestRoute = btd[1]
|
|
1198
|
+
bestScore = btd[5]
|
|
1199
|
+
bestTradeScore = bestRoute.score + bestScore
|
|
1200
|
+
newTradeScore = route.score + score
|
|
1201
|
+
if bestTradeScore > newTradeScore:
|
|
1202
|
+
continue
|
|
1203
|
+
if bestTradeScore == newTradeScore:
|
|
1204
|
+
bestLy = btd[4]
|
|
1205
|
+
if bestLy <= dest.distLy:
|
|
1206
|
+
continue
|
|
1207
|
+
|
|
1208
|
+
bestToDest[dstID] = (
|
|
1209
|
+
dstStation,
|
|
1210
|
+
route,
|
|
1211
|
+
trade,
|
|
1212
|
+
dest.via,
|
|
1213
|
+
dest.distLy,
|
|
1214
|
+
score,
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
if heartbeat_enabled:
|
|
1218
|
+
sys.stderr.write("\n"); sys.stderr.flush()
|
|
1219
|
+
|
|
1220
|
+
if connections == 0:
|
|
1221
|
+
raise NoHopsError("No destinations could be reached within the constraints.")
|
|
1222
|
+
|
|
1223
|
+
result = []
|
|
1224
|
+
for (dst, route, trade, jumps, _, score) in bestToDest.values():
|
|
1225
|
+
result.append(route.plus(dst, trade, jumps, score))
|
|
1226
|
+
|
|
1227
|
+
return result
|