Skip to content

Commit

Permalink
Early working version
Browse files Browse the repository at this point in the history
  • Loading branch information
kfsone committed Jul 7, 2014
0 parents commit 2458f81
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__

Binary file added TradeDangerous.accdb
Binary file not shown.
224 changes: 224 additions & 0 deletions trade.py
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()
94 changes: 94 additions & 0 deletions tradedb.py
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)

0 comments on commit 2458f81

Please sign in to comment.