-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2458f81
Showing
4 changed files
with
320 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
__pycache__ | ||
|
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
#!/usr/bin/env python | ||
# | ||
# We can easily predict the best run from A->B, but the return trip from B->A might | ||
# not give us the best profit. | ||
# The goal here, then, is to find the best multi-hop route. | ||
|
||
# Potential Optimization: Two routes with the same dest at the same hop, | ||
# eliminate the one with the lowest score at that hop | ||
|
||
###################################################################### | ||
# Imports | ||
|
||
import argparse | ||
from pprint import pprint, pformat | ||
import itertools | ||
from collections import deque | ||
from heapq import heappush, heappushpop | ||
from math import ceil, floor | ||
|
||
# Forward decls | ||
args = None | ||
profitCache = dict() | ||
originID, finalStationID, viaStationID = None, None, None | ||
originName, destName, viaName = "Any", "Any", "Any" | ||
origins = [] | ||
|
||
###################################################################### | ||
# DB Setup | ||
|
||
# Connect | ||
from tradedb import TradeDB, Trade, Station | ||
tdb = TradeDB("C:\\Dev\\trade\\TradeDangerous.accdb") | ||
|
||
###################################################################### | ||
# Classes | ||
|
||
class Route(object): | ||
""" Describes a series of CargoRuns, that is CargoLoads | ||
between several stations. E.g. Chango -> Gateway -> Enterprise | ||
""" | ||
def __init__(self, route, hops, gainCr): | ||
self.route = list(route) | ||
self.hops = hops | ||
self.gainCr = gainCr | ||
|
||
def __lt__(self, rhs): | ||
return self.gainCr < rhs.gainCr | ||
def __eq__(self, rhs): | ||
return self.gainCr == rhs.gainCr | ||
|
||
def __repr__(self): | ||
src = tdb.stations[self.route[0]] | ||
credits = args.credits | ||
gainCr = 0 | ||
route = self.route | ||
|
||
str = "%s -> %s:\n" % (src, tdb.stations[route[-1]]) | ||
for i in range(len(route) - 1): | ||
hop = self.hops[i] | ||
str += " @ %-20s Buy" % tdb.stations[route[i]] | ||
for item in hop[0]: | ||
str += " %d*%s," % (item[1], item[0]) | ||
str += "\n" | ||
gainCr += hop[1] | ||
|
||
str += " $ %s %dcr + %dcr => %dcr total" % (tdb.stations[route[-1]], credits, gainCr, credits + gainCr) | ||
|
||
return str | ||
|
||
###################################################################### | ||
|
||
def parse_command_line(): | ||
global args, origins, originID, finalStationID, viaStationID, originName, destName, viaName | ||
|
||
parser = argparse.ArgumentParser(description='Trade run calculator') | ||
parser.add_argument('--from', dest='origin', metavar='<Origin>', help='Specifies starting system/station', required=False) | ||
parser.add_argument('--to', dest='dest', metavar='<Destination>', help='Specifies final system/station', required=False) | ||
parser.add_argument('--via', dest='via', metavar='<ViaStation>', help='Require specified station to be en-route', required=False) | ||
parser.add_argument('--credits', metavar='<Balance>', help='Number of credits to start with', type=int, required=True) | ||
parser.add_argument('--hops', metavar="<Hops>", help="Number of hops to run", type=int, default=2, required=False) | ||
parser.add_argument('--capacity', metavar="<Capactiy>", help="Maximum capacity of cargo hold", type=int, default=4, required=False) | ||
parser.add_argument('--insurance', metavar="<Credits>", help="Reserve at least this many credits", type=int, default=0, required=False) | ||
parser.add_argument('--margin', metavar="<Error Margin>", help="Reduce gains by this much to provide a margin of error for market fluctuations (e.g. 0.25 reduces gains by 1/4). 0<=m<=0.25. Default: 0.02", default=0.02, type=float, required=False) | ||
parser.add_argument('--debug', help="Enable verbose output", default=False, required=False, action='store_true') | ||
parser.add_argument('--routes', metavar="<MaxRoutes>", help="Maximum number of routes to show", type=int, default=1, required=False) | ||
|
||
args = parser.parse_args() | ||
|
||
if args.hops < 1: | ||
raise ValueError("Minimum of 1 hop required") | ||
if args.hops > 10: | ||
raise ValueError("Too many hops without more optimization") | ||
|
||
if args.origin: | ||
originName = args.origin | ||
originID = tdb.get_station_id(originName) | ||
origins = [ originID ] | ||
else: | ||
origins = list(stations.keys()) | ||
|
||
if args.dest: | ||
destName = args.dest | ||
finalStationID = tdb.get_station_id(destName) | ||
if args.hops == 1 and originID and finalStationID and originID == finalStationID: | ||
raise ValueError("More than one hop required to use same from/to destination") | ||
|
||
if args.via: | ||
if args.hops < 2: | ||
raise ValueError("Minimum of 2 hops required for a 'via' route") | ||
viaName = args.via | ||
viaStationID = tdb.get_station_id(viaName) | ||
if args.hops == 2: | ||
if viaStationID == originID: | ||
raise ValueError("3+ hops required to go 'via' the origin station") | ||
if viaStationID == finalStationID: | ||
raise ValueError("3+ hops required to go 'via' the destination station") | ||
if args.hops <= 3: | ||
if viaStationID == originID and viaStationID == finalStationID: | ||
raise ValueError("4+ hops required to go 'via' the same station as you start and end at") | ||
|
||
if args.credits < 0: | ||
raise ValueError("Invalid (negative) value for initial credits") | ||
|
||
if args.capacity < 0: | ||
raise ValueError("Invalid (negative) cargo capacity") | ||
|
||
if args.insurance and args.insurance >= (args.credits + 30): | ||
raise ValueError("Insurance leaves no margin for trade") | ||
|
||
if args.routes < 1: | ||
raise ValueError("Maximum routes has to be 1 or higher") | ||
|
||
return args | ||
|
||
###################################################################### | ||
# Processing functions | ||
|
||
def try_combinations(capacity, credits, tradeList): | ||
best, bestCr = [], 0 | ||
for idx, trade in enumerate(tradeList): | ||
itemCostCr = trade.costCr | ||
maximum = min(capacity, credits // itemCostCr) | ||
if maximum > 0 : | ||
best.append([trade, maximum]) | ||
capacity -= maximum | ||
credits -= maximum * itemCostCr | ||
bestCr += trade.gainCr | ||
if not capacity or not credits: | ||
break | ||
return [ best, bestCr ] | ||
|
||
|
||
def get_profits(srcID, dstID, startCr): | ||
src = tdb.stations[srcID] | ||
|
||
if args.debug: print("%s -> %s with %dcr" % (src, tdb.stations[dstID], startCr)) | ||
|
||
# Get a list of what we can buy | ||
trades = src.links[dstID] | ||
return try_combinations(args.capacity, startCr, trades) | ||
|
||
|
||
def generate_routes(): | ||
q = deque([[origID] for origID in origins]) | ||
hops = args.hops | ||
while q: | ||
# get the most recent partial route | ||
route = q.pop() | ||
# furthest station on the route | ||
lastStation = tdb.stations[route[-1]] | ||
if len(route) >= hops: # destination | ||
# upsize the array so we can reuse the slot. | ||
route.append(0) | ||
for dstID in lastStation.links: | ||
route[-1] = dstID | ||
yield route | ||
else: | ||
for dstID in lastStation.links: | ||
q.append(route + [dstID]) | ||
|
||
def main(): | ||
parse_command_line() | ||
|
||
print("From %s via %s to %s with %d credits for %d hops" % (originName, viaName, destName, args.credits, args.hops)) | ||
|
||
startCr = args.credits - args.insurance | ||
routes = [] | ||
for route in generate_routes(): | ||
# Do we have a specific destination requirement? | ||
if finalStationID and route[-1] != finalStationID: | ||
continue | ||
# Do we have a travel-via requirement? | ||
if viaStationID and not viaStationID in route[1:-2]: | ||
continue | ||
credits = startCr | ||
gainCr = 0 | ||
hops = [] | ||
for i in range(0, len(route) - 1): | ||
srcID, dstID = route[i], route[i + 1] | ||
if args.debug: print("hop %d: %d -> %d" % (i, srcID, dstID)) | ||
bestTrade = get_profits(srcID, dstID, credits + gainCr) | ||
if not bestTrade: | ||
break | ||
hops.append(bestTrade) | ||
gainCr += int(bestTrade[1] * (1.0 - args.margin)) | ||
if len(hops) + 1 < len(route): | ||
continue | ||
if len(routes) >= len(tdb.stations) * 3: | ||
if min(routes).gainCr <= gainCr: | ||
heappushpop(routes, Route(route, hops, gainCr)) | ||
else: | ||
heappush(routes, Route(route, hops, gainCr)) | ||
assert credits + gainCr > startCr | ||
|
||
if not routes: | ||
print("No routes match your selected criteria.") | ||
return | ||
|
||
for i in range(0, min(len(routes), args.routes)): | ||
print(routes[i]) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
#!/usr/bin/env python | ||
# TradeDangerousDB | ||
# Loads system, station, item and trade-price data from the db | ||
|
||
###################################################################### | ||
# Imports | ||
|
||
import pypyodbc | ||
|
||
###################################################################### | ||
# Classes | ||
|
||
class Trade(object): | ||
""" Describes what it would cost and how much you would gain | ||
when selling an item between two specific stations. """ | ||
def __init__(self, item, costCr, gainCr): | ||
self.item = item | ||
self.costCr = costCr | ||
self.gainCr = gainCr | ||
|
||
def describe(self): | ||
print(self.item, self.costCr, self.gainCr, self.value) | ||
|
||
def __repr__(self): | ||
return "%s@%d+%d" % (self.item, self.costCr, self.gainCr) | ||
|
||
|
||
class Station(object): | ||
""" Describes a station and which stations it links to, the links themselves | ||
also describe what products can be sold to the destination and how much | ||
profit can be earned by doing so. These are stored in gain order so that | ||
we can easily tell if we can afford to fill up on the most expensive | ||
item. """ | ||
def __init__(self, ID, system, station): | ||
self.ID, self.system, self.station = ID, system.replace(' ', ''), station.replace(' ', '') | ||
self.links = {} | ||
|
||
def addTrade(self, dstID, item, costCr, gainCr): | ||
""" Add a Trade entry from this to a destination station """ | ||
if not dstID in self.links: | ||
self.links[dstID] = [] | ||
self.links[dstID].append(Trade(item, costCr, gainCr)) | ||
|
||
def organizeTrades(self): | ||
for station in self.links: | ||
items = self.links[station] | ||
items.sort(key=lambda trade: trade.gainCr, reverse=True) | ||
|
||
def __str__(self): | ||
str = self.system + " " + self.station | ||
return str | ||
|
||
|
||
class TradeDB(object): | ||
def __init__(self, path="TradeDangerous.accdb"): | ||
self.path = "Driver={Microsoft Access Driver (*.mdb, *.accdb)};DBQ=" + path | ||
self.load() | ||
|
||
def load(self): | ||
# Connect to the database | ||
conn = pypyodbc.connect(self.path) | ||
cur = conn.cursor() | ||
|
||
cur.execute('SELECT id, system, station FROM Stations') | ||
# Station lookup by ID | ||
self.stations = { row[0]: Station(row[0], row[1], row[2]) for row in cur} | ||
# StationID lookup by System Name | ||
self.systemIDs = { value.system.upper(): key for (key, value) in self.stations.items() } | ||
# StationID lookup by Station Name | ||
self.stationIDs = { value.station.upper(): key for (key, value) in self.stations.items() } | ||
|
||
""" Populate 'items' from the database """ | ||
cur.execute('SELECT id, item FROM Items') | ||
self.items = { row[0]: row[1] for row in cur } | ||
|
||
""" Populate the station list with the profitable trades between stations """ | ||
cur.execute('SELECT src.station_id, dst.station_id, src.item_id, src.buy_cr, dst.sell_cr - src.buy_cr' | ||
' FROM Prices AS src INNER JOIN Prices AS dst ON src.item_id = dst.item_id' | ||
' WHERE src.station_id <> dst.station_id AND src.buy_cr > 0 AND dst.sell_cr > src.buy_cr' | ||
' AND src.ui_order > 0 AND dst.ui_order > 0' | ||
' ORDER BY (dst.sell_cr - src.buy_cr) / src.buy_cr DESC') | ||
for row in cur: | ||
self.stations[row[0]].addTrade(row[1], self.items[row[2]], row[3], row[4]) | ||
|
||
for station in self.stations.values(): | ||
station.organizeTrades() | ||
|
||
def get_station_id(self, name): | ||
upperName = name.upper() | ||
if upperName in self.systemIDs: | ||
return self.systemIDs[upperName] | ||
elif upperName in self.stationIDs: | ||
return self.stationIDs[upperName] | ||
raise ValueError("Unrecognized system/station name '%s'" % name) |