diff --git a/README b/README new file mode 100644 index 0000000..de03880 --- /dev/null +++ b/README @@ -0,0 +1,30 @@ +Penny Auction Observer can record penny auctions taking place on penny auction websites. + +How the F do you use this thing? + +ruby bin/pa_auction_bot.rb -- help + + +What the F does it do? + +I "observes" a live penny auction, and executes blocks of ruby code at the key events. +The blocks of ruby code are stored in interchangeable hook files. +The key events are: +- Monitoring a new auction +- New bids occur on a live auction +- A timer threshold is reached +- The auction ends + + +What the F is it useful for? + +- Creating logs of penny auctions +- Creating bots which can bid on penny auctions (not recommended by the authors of this tool) +- Doing research on penny auctions + + +Why the F did you make it? + +Penny auctions are an interesting thing. +In my opinion they exploit weaknesses in the human psyche, much the same way gambling does. +I wanted to collect data and study this phenomenon, strictly for my own fun. diff --git a/bin/pa_auction_bot.rb b/bin/pa_auction_bot.rb new file mode 100644 index 0000000..ebe1974 --- /dev/null +++ b/bin/pa_auction_bot.rb @@ -0,0 +1,56 @@ +require 'optparse' +#require 'rubygems' + + +#Deal with the cmd line +def parse_cmd_line + options = { + :hooks_file => 'bots/csv_logger.rb', + :site => 'QUIBIDS', + } + + optparse = OptionParser.new do |opts| + opts.banner = %Q| + Penny Auction Observer + + example: ruby -I lib bin/pa_auction_but.rb bots/csv_logger.rb + | + + opts.on("-b", "--bot-file=FILE_NAME", + "specify the bot file to execute, default is '#{options[:hooks_file]}'") { |filename| options[:hooks_file] = filename} + + opts.on("-a", "--auction-id=AUCTION_ID", + "the auction id") { |id| options[:auction_id] = id } + + opts.on("-s", "--site=SITE", + "Specify the site to use, default is '#{options[:site]}'") { |site| options[:site] = site} + + + end + optparse.parse! + options +end +options = parse_cmd_line + + + +require 'pa_site' +require 'pa_observer' + +#setup the object +auction_id = options[:auction_id] +load options[:hooks_file] +pa_site = QB_Site.new options[:site].upcase +pa_site.start auction_id + + + +auction_observer = QB_Observer.new pa_site + + +auction_observer.hooks[:on_new_bids] = OnNewBids +auction_observer.hooks[:on_new_auction] = OnNewAuction +auction_observer.hooks[:on_auction_end] = OnAuctionEnd +auction_observer.hooks[:on_timer_threshold] = OnTimerThreshold + +auction_observer.observe_auction diff --git a/bots/basic_watcher.rb b/bots/basic_watcher.rb new file mode 100644 index 0000000..1df7114 --- /dev/null +++ b/bots/basic_watcher.rb @@ -0,0 +1,38 @@ +## Logs a QuiBids Auction to STDOUT +# + +## NEW BIDS +#data +num_bids = 0 +last_amt = -1.0 + +## AUCTION HOOKS +OnNewAuction = lambda {|auction_name| + last_amt = -1.0 + num_bids = 0 + puts "Now watching new auction: #{auction_name}" +} + +OnAuctionEnd = lambda {|auction_name| + puts "Done watching auction: #{auction_name}" +} + + +OnNewBids = lambda {|new_bids| + new_bids.each do |bid| + num_bids += 1 + puts "NEW BID: '#{bid[:bidder]}' : '#{bid[:amt]}' : '#{bid[:type]}' : #{bid[:last_secs]} : #{last_amt}" + end + last_amt = new_bids.last[:amt] if new_bids.count > 0 + puts "Processed #{new_bids.count} new bids" +} + + + +## TIMER THRESHOLD +num_hits = 0 +OnTimerThreshold = lambda {|secs, browser| + #browser.bid + num_hits += 1 + puts "TIMER THRESHOLD HIT: #{num_hits} so far" +} diff --git a/bots/bidder.rb b/bots/bidder.rb new file mode 100644 index 0000000..196a1c3 --- /dev/null +++ b/bots/bidder.rb @@ -0,0 +1,48 @@ +## Logs a QuiBids Auction to CSV +# + +require 'bidder_model' + +## NEW BIDS +#data +num_bids = 0 +num_skips = 0 +last_amt = -1.0 + + +## AUCTION HOOKS +OnNewAuction = lambda {|auction_name| + + @model = QB_Model.new + puts "Initialized '#{auction_name}'" +} + +OnAuctionEnd = lambda {|auction_name| + puts "Auction End" +} + + +OnNewBids = lambda {|new_bids| + + @model.process_new_bids new_bids + + +} + + + +## TIMER THRESHOLD +num_bids = 0 +OnTimerThreshold = lambda {|secs, browser| + if @model.would_bid + #browser.bid + num_bids += 1 + puts "TIMER HIT: #{num_bids} so far" + print "\a" + else + num_skips += 1 + puts "SKIP: #{num_skips} so far" + end +} + + diff --git a/bots/bidder_model.rb b/bots/bidder_model.rb new file mode 100644 index 0000000..f51080b --- /dev/null +++ b/bots/bidder_model.rb @@ -0,0 +1,69 @@ + +class QB_Model + def initialize + @bids = [] + @bidders = {} + + @num_bids = 0 + @last_amt = 0.00 + + @uniques = {:u10 => 10, :u20 => 20} + @autoq = 9999 + end + + def process_new_bids new_bids + new_bids.each do |bid| + + @bids << bid + + if @bidders.has_key? bid[:bidder] + @bidders[bid[:bidder]][:count] += 1 + else + @bidders[bid[:bidder]] = { + :count => 1 + } + end + + @num_bids += 1 + puts "NEW BID #{@num_bids}\t #{bid[:bidder]}\t #{bid[:amt]}\t #{bid[:type]}\t #{bid[:last_secs]}\t:: #{@bidders[bid[:bidder]][:count]} so far" + end + + ##determine unique bidders + @uniques = {} + bidders = @bids.reverse.map{|b| b[:bidder]} + @uniques[:u10] = bidders[0, 10].uniq.length + @uniques[:u20] = bidders[0, 20].uniq.length + #@uniques[:u30] = bidders[0, 30].uniq.length + #@uniques[:u40] = bidders[0, 40].uniq.length + #@uniques[:u50] = bidders[0, 50].uniq.length + + puts "Uniques: #{@uniques.inspect}" + + + ##determine ratio of Auto to Manual + types = @bids.reverse.map{|b| b[:type]} + + @autoq = 0 + plus = 10 + while (plus > 0) + @autoq += plus if types[10-plus] == :automatic + plus -= 1 + end + + puts "AUTO Q: #{@autoq}" + + @last_amt = new_bids.last[:amt] if new_bids.length > 0 + puts "Processed #{new_bids.length} new bids" + end + + def would_bid + + return false if @num_bids < 25 + return false if @uniques[:u10] > 6 + return false if @uniques[:u20] > 9 + return false if @autoq > 30 + + return true + end +end + diff --git a/bots/csv_logger.rb b/bots/csv_logger.rb new file mode 100644 index 0000000..7a39375 --- /dev/null +++ b/bots/csv_logger.rb @@ -0,0 +1,55 @@ +## Logs a QuiBids Auction to CSV +# + +## NEW BIDS +#data +num_bids = 0 +last_amt = -1.0 + +### HELPERS +def init_csv filename + filename = 'csv/' + filename + '.' + Time.now.strftime("%m%d%Y_%H%M") + '.csv' + @csv_writer = File.open(filename, 'w') + puts "Opened csv '#{filename}'" +end + +def write_csv line_array + str = "" + line_array.each {|f| str += "#{f.to_s}, "} + @csv_writer.puts str.sub(/, $/,'') +end + + +## AUCTION HOOKS +OnNewAuction = lambda {|auction_name| + last_amt = -1.0 + num_bids = 0 + init_csv auction_name +} + +OnAuctionEnd = lambda {|auction_name| + @csv_writer.close + puts "Closed csv" +} + + +OnNewBids = lambda {|new_bids| + new_bids.each do |bid| + num_bids += 1 + puts "NEW BID: '#{bid[:bidder]}' : '#{bid[:amt]}' : '#{bid[:type]}' : #{bid[:last_secs]} : #{last_amt}" + write_csv [num_bids, Time.now.strftime("%H:%M:%S"), bid[:amt], bid[:bidder], bid[:type], bid[:last_secs], last_amt] + end + last_amt = new_bids.last[:amt] if new_bids.count > 0 + puts "Processed #{new_bids.count} new bids" +} + + + +## TIMER THRESHOLD +num_hits = 0 +OnTimerThreshold = lambda {|secs, browser| + #browser.bid + num_hits += 1 + puts "TIMER HIT: #{num_hits} so far" + #print "\a" +} diff --git a/lib/pa_observer.rb b/lib/pa_observer.rb new file mode 100644 index 0000000..e24409f --- /dev/null +++ b/lib/pa_observer.rb @@ -0,0 +1,86 @@ +# This class watches an auction + +require 'pa_site' + +class QB_Observer + def initialize pa_site + @pa_site = pa_site + + @hooks = {} #contains lambdas which are called in observice auction loop + end + + attr_accessor :hooks + + def auction_name + @pa_site.auction_name + end + + def process_event name, *args + @hooks[name].call(*args) if @hooks.has_key? name + end + + def get_new_bids + enhanced_bids = @pa_site.get_new_bids + enhanced_bids.each {|b| b[:last_secs] = @last_secs } + enhanced_bids + end + + def observe_auction + @pa_site.initialize_auction + + process_event :on_new_auction, auction_name + + #DATA USED IN LOOP + cur_secs = 0 + @last_secs = -1 + secs_since_refresh = 0 + + while ( true ) + begin + cur_secs = @pa_site.seconds_left + rescue + puts $! + break + end + + #If the timer changed since last observation + if (cur_secs != @last_secs ) + + #If the timer is almost out (bid) + if cur_secs < 2 + process_event :on_timer_threshold, cur_secs, @pa_site + end + + puts " - " + cur_secs.to_s + + #If the timer went up, then we have new bids to process + if (cur_secs > @last_secs ) + #Need to refresh the browser periodically to prevent inactivity popups + if secs_since_refresh > 900 + @pa_site.refresh_auction + secs_since_refresh = 0 + end + + #keep getting new bids until there isn't any left to get, sometimes the time spent in new bid event is enough for new bids to show up... dont want to wait until another bid + new_bids = get_new_bids + while (new_bids.count > 0) + process_event :on_new_bids, new_bids + sleep 1.0 + new_bids = get_new_bids + end + + end + + #refresh the browser periodically to prevent innactivity popups + secs_since_refresh += 1 + end + + @last_secs = cur_secs + sleep 0.05 + end + puts "End of Auction" + process_event :on_auction_end, auction_name + end + +end + diff --git a/lib/pa_site.rb b/lib/pa_site.rb new file mode 100644 index 0000000..14047e2 --- /dev/null +++ b/lib/pa_site.rb @@ -0,0 +1,34 @@ +#Handles all Interactions with the site/browser through watir +# *Auction stuff it does* +# - Gets timer value (in seconds) +# - Gets bids above a certain bid amt +# - Gets bids that are new since last check +# - Presses the bid button (TODO) +# - Detects auction end (by raising exception) +# + +require 'watir-webdriver' + + +class String + def pa_calc_amt + self.sub(/\$/,'').to_f + end +end + + +class QB_Site + + + def initialize binding_set + QB_Site.load_biding_set binding_set + end + + def self.load_biding_set binding_set + load File.join('site_bindings', "#{binding_set.downcase}.rb") + include Kernel.const_get binding_set + end + +end + + diff --git a/site_bindings/quibids.rb b/site_bindings/quibids.rb new file mode 100644 index 0000000..b85ec3a --- /dev/null +++ b/site_bindings/quibids.rb @@ -0,0 +1,98 @@ +require 'watir-webdriver' + +#this module implements +module QUIBIDS + + @browser = nil + @last_amt = nil + @auction_els = nil + + def start auction_id + @browser = Watir::Browser.new 'firefox' + @browser.goto "http://quibids.com/auctions/#{auction_id}" + end + + def auction_name + name = @browser.title + puts "auction name is '#{name}'" + name + end + + def initialize_auction + #init some data + @last_amt = -1.0 + @auction_els = { + :timer => @browser.div(:class => /timer2/ ), + :history => @browser.div(:id => 'bidding-history' ).table, + :bid_btn => @browser.link(:class => /^bid/ ), + } + + #confirm all the elements are accessible + @auction_els.each_pair do |name, el| + puts "Checking #{name}" + (0...5).each do |i| + #break if el.exist? && el.visible? + break if el.exist? + puts "not found on attempt #{i}" + sleep 1.0 + end + puts "Found #{name}" + end + end + + def refresh_auction + @browser.refresh + checks = 0 + while not(@auction_els[:timer].exist?) + sleep 0.5 + checks += 1 + raise "Refresh failed to find auction again?!" if checks > 20 + end + while ( text(:timer) !~ /\d\d:\d\d:\d\d/ ) + sleep 0.5 + checks += 1 + raise "Refresh failed to find auction again?!" if checks > 20 + end + end + + def bid + @auction_els[:bid_btn].click + end + + def seconds_left + begin + timer_str = @auction_els[:timer].text + rescue Selenium::WebDriver::Error::ObsoleteElementError + puts "Timer text retrieval failed, breifly pausing and retrying..." + sleep 0.1 + timer_str = @auction_els[:timer].text + end + + unless timer_str.match /\d\d:\d\d:\d\d/ + #puts "got timer string: '#{timer_str}'" + raise "Not a timer string" if timer_str =~ /\S/ + end + h, m, s = timer_str.split(':').map{|n| n.to_i} + (h * 3600) + (m * 60) + (s) + end + + + #improve this, have it parse static html to improve the data capture and eliminate live updating problem + def get_new_bids + cur_amt = @last_amt + bids = [] + @auction_els[:history].hashes.reverse.each do |bid_row| + new_bid = { + :bidder => bid_row['BIDDER'], + :amt => bid_row['BID'].pa_calc_amt, + :type => bid_row['TYPE'] =~ /BidOMatic/ ? :automatic : :manual, + } + if new_bid[:amt] > @last_amt + bids << new_bid + @last_amt = new_bid[:amt] + end + end + bids + end + +end \ No newline at end of file