42
\$\begingroup\$

NOTICE: This challenge is now closed: I will no longer be updating the leaderboard and will not change the accepted answer. However, you are free to run the controller and update the leaderboard yourself, if you wish.

Join the chat!

Introduction

Good evening, traders! You are all traders for the golfing company PPCG. Your task is to make as much money as possible.

Challenge

Write a program that buys and sells shares on the Stack Exchange Stock Exchange with the aim of making as much money as possible.

Gameplay

All players will start with 5 shares and $100 in their bank. The game always starts with a share price of $10.

Each game will have 1000 rounds where the first round is round 1. In each round, your program will be supplied four arguments as input: the current share price, the number of shares you hold, the amount of money you own and the round number (1-indexed).

For example, if my program is test1.py, the share price is 100, the number of shares I hold is 3, the amount of money I have is 1200, and the round number is 576, my program will be run like so:

python test1.py 100 3 1200 576

In a round, the share price given to each player will be the same. This doesn't change until the end of the round.

In response, the player must print their command. There are two options:

  • Buy shares: This command is given as bn where n is the number of shares you wish to buy. For example, if you want to buy 100 shares, you would output:
b100

When buying shares, you are allowed an overdraft of up to $1000. If you try to buy enough shares that exceed this overdraft (your bank balance goes below $-1000), you will be declared bankrupt. This means that you will lose all of your shares and your balance will be set to $50.

The share price will be unaffected by your command if you go bankrupt.

(If your balance is $-1000, you are not bankrupt. However if your balance is $-1001, you are bankrupt)

  • Sell shares: This command is given as sn where n is the number of shares you wish to sell. For example, if you want to sell 100 shares, you would output:
s100

You may not sell more shares than you own. If you try to do this, your request will be denied, and you will skip the round.

If you want to skip the round and do nothing, output either b0 or s0.

Your request will be denied if you try to buy or sell a negative number of shares and/or a non-integer number of shares.

After 5 rounds, at the end of each round, all players will be payed a dividend, the value of which is 5% of the mean average share price of the last 5 rounds.

How does it work?

Initially the share price will be $10. At the end of each round, it will be recalculated using the formula:

$$\text{New Share Price} = \text{Old Share Price} + (\text{Number of shares bought}-\text{Number of shares sold})$$

The share price will be limited so that it never falls below $1.

To prevent overly rapid change, the change in share price is limited to a maximum of \$\pm$200\$.

Rules

  • Your program must have a name


  • Your program is allowed a single text file for data storage. It must be stored in the same folder as your program


  • Include in your answer details of how to run your program


  • This KotH is open to all programming languages that are free-to-use and can be run on Windows 10


  • Your score is based solely on the contents of your balance. Any money locked up in shares will not be counted


  • You may edit your program at any time. Before each game, the latest code will be saved and compiled


  • You should not write code which specifically targets another bot.

Controller

The controller is written in Python and can be found here: https://gist.github.com/beta-decay/a6abe40fc9f4ff6cac443395377ec31f

At the end it will print a leaderboard and display a graph of how the share price changed throughout the game.

For example, when two random bots were playing

Winning

The player with the highest amount of money in their balance at the end of the last game wins.

Leaderboard

Game 4: 16:14 10/08/2018

Name                                Balance

Experienced Greedy Idiot            $14802860126910608746226775271608441476740220190868405578697473058787503167301288688412912141064764060957801420415934984247914753474481204843420999117641289792179203440895025689047561483400211597324662824868794009792985857917296068788434607950379253177065699908166901854516163240207641611196996217004494096517064741782361827125867827455285639964058498121173062045074772914323311612234964464095317202678432969866099864014974786854889944224928268964434751475446606732939913688961295787813863551384458839364617299883106342420461998689419913505735314365685264187374513996061826694192786379011458348988554845036604940421113739997490412464158065355335378462589602228039730
Equalizer                           $763185511031294813246284506179317396432985772155750823910419030867990447973211564091988995290789610193513321528772412563772470011147066425321453744308521967943712734185479563642323459564466177543928912648398244481744861744565800383179966018254551412512770699653538211331184147038781605464336206279313836606330
Percentage Trader                   $448397954167281544772103458977846133762031629256561243713673243996259286459758487106045850187688160858986472490834559645508673466589151486119551222357206708156491069820990603783876340193236064700332082781080188011584263709364962735827741094223755467455209136453381715027369221484319039100339776026752813930
OYAIB                               $8935960891618546760585096898089377896156886097652629690033599419878768424984255852521421137695754769495085398921618469764914237729576710889307470954692315601571866328742408488796145771039574397444873926883379666840494456194839899502761180282430561362538663182006432392949099112239702124912922930
Chimps on a Typewriter              $176504338999287847159247017725770908273849738720252130115528568718490320252556133502528055177870
Greedy B*****d                      $17689013777381240
Illiterate Dividend Investor        $2367418699671980
Lucky Number 6                      $4382725536910
Lone Accountant                     $90954970320
Buy/Reinvest                        $127330
Technical Analysis Robot            $126930
Dollar Cost Averager                $106130
Fibonacci                           $69930
Novice Broker                       $28130
Buy Low                             $6130
Naive Statistician                  $6130
Fallacious Gambler                  $6130
Passive Trader                      $4980
Half More or Nothing                $4920
Monkeys on a Typewriter             $66

View graphs of each contestant


Related but the gameplay and winning criterion are very different to this challenge.

\$\endgroup\$
9
  • \$\begingroup\$ Comments are not for extended discussion; this conversation has been moved to chat. \$\endgroup\$
    – Dennis
    Aug 3, 2018 at 13:48
  • \$\begingroup\$ For me the formula shows as [Math Processing Error] in red. Is it the same for others as well? If so maybe it's an issue with the question. \$\endgroup\$ Aug 3, 2018 at 13:49
  • 2
    \$\begingroup\$ It might be worth averaging results over say, 10-100 games to reduce the influence of luck. Or maybe that would be changing the challenge too much. \$\endgroup\$
    – mbrig
    Aug 3, 2018 at 14:28
  • 1
    \$\begingroup\$ Would it be possible to have the scores be log2/log10? It would make it much easier to compare the scores. (I browse with my phone, and the exponents disappeared off screen) \$\endgroup\$
    – user77406
    Aug 3, 2018 at 21:09
  • 1
    \$\begingroup\$ I think that even 10-100 is too few, but I like to run a lot of games. To make that possible, you'd need to change the format of the challenge, which is out of scope now. \$\endgroup\$ Aug 4, 2018 at 17:49

21 Answers 21

19
\$\begingroup\$

Chimps On A Typewriter

import random
from sys import argv

share_price = int(argv[1])
share_count = int(argv[2])
balance = float(argv[3])

x = random.random()
if x < 0.5:
    max_buy = balance / share_price
    buy_count = int(max_buy * random.random())
    print('b' + str(buy_count))
else:
    sell_count = int(share_count * random.random())
    print('s' + str(sell_count))

Chimps are smarter than monkeys, they won't buy stocks they can't afford, or sell stocks they don't have.

Still pretty random otherwise though.

Run with python3, but should(?) work with python2 as well

\$\endgroup\$
5
  • 1
    \$\begingroup\$ They may be smarter, but are they luckier? \$\endgroup\$
    – Woohoojin
    Aug 2, 2018 at 19:44
  • \$\begingroup\$ In all of my tests this one has come out on top, so yes \$\endgroup\$
    – Mayube
    Aug 2, 2018 at 23:02
  • 27
    \$\begingroup\$ I'm extremely curious how this won the first round by more than 20 orders of magnitude \$\endgroup\$
    – mbrig
    Aug 3, 2018 at 14:30
  • \$\begingroup\$ I like to put it down to the art of simplicity. Everyone else is over-engineering their bots. \$\endgroup\$
    – Mayube
    Aug 3, 2018 at 19:27
  • 1
    \$\begingroup\$ This got so much love, by mistake :P \$\endgroup\$
    – Night2
    Aug 8, 2018 at 8:38
11
\$\begingroup\$

The Experienced Greedy Idiot

PHP, tested on PHP >= 7, should work on previous ones as well.

<?php

class StickExchange
{
    private $dbFile;
    private $sharePrice;
    private $shares;
    private $balance;
    private $overdraft;

    public function __construct($sharePrice, $shares, $balance, $round)
    {
        $this->dbFile = __FILE__ . '.txt';
        $this->sharePrice = gmp_init($sharePrice);
        $this->shares = gmp_init($shares);
        $this->balance = gmp_init($this->parseScientificNotationToInt($balance));
        $this->overdraft = gmp_init(1000);

        $action = 'b';

        if ($round == 1) {
            $this->buy();
        } elseif ($round == 1000) {
            $this->sell();
        } else {
            $content = $this->getDbContent();
            $lastPrice = gmp_init($content['price']);
            $secondLastPrice = gmp_init($content['last_price']);
            $lastAction = $content['action'];

            $shareAndLastCmp = gmp_cmp($this->sharePrice, $lastPrice);
            $lastAndSecondLastCmp = gmp_cmp($lastPrice, $secondLastPrice);

            if ($shareAndLastCmp > 0 && $lastAndSecondLastCmp > 0) {
                if ($lastAction == 'b') {
                    $this->sell();
                    $action = 's';
                } else {
                    $this->buy();
                }
            } elseif ($shareAndLastCmp < 0 && $lastAndSecondLastCmp < 0) {
                if ($lastAction == 'b') {
                    $this->sell();
                    $action = 's';
                } else {
                    $this->skip();
                }
            } elseif ($shareAndLastCmp > 0) {
                $this->sell();
                $action = 's';
            } elseif ($shareAndLastCmp < 0) {
                $this->buy();
            } else {
                $this->skip();
            }
        }

        $this->setDbContent([
            'action' => $action,
            'price' => gmp_strval($this->sharePrice),
            'last_price' => isset($lastPrice) ? gmp_strval($lastPrice) : '0',
        ]);
    }

    private function parseScientificNotationToInt($number)
    {
        if (strpos($number, 'e+') !== false) {
            $sParts = explode('e', $number);
            $parts = explode('.', $sParts[0]);
            $exp = (int)$sParts[1];

            if (count($parts) > 1) {
                $number = $parts[0] . $parts[1];
                $exp -= strlen($parts[1]);
            } else {
                $number = $parts[0];
            }

            $number = gmp_init($number);
            $pow = gmp_pow(gmp_init(10), $exp);
            return gmp_strval(gmp_mul($number, $pow));
        } elseif (strpos($number, 'e-') !== false) {
            return sprintf('%d', $number);
        } else {
            $parts = explode('.', $number);
            return $parts[0];
        }
    }

    private function getDbContent()
    {
        return unserialize(file_get_contents($this->dbFile));
    }

    private function setDbContent($content)
    {
        file_put_contents($this->dbFile, serialize($content));
    }

    private function buy()
    {
        $realBalance = gmp_add($this->balance, $this->overdraft);
        $sharesToBuy = gmp_div($realBalance, $this->sharePrice);
        $this->stdout('b' . gmp_strval($sharesToBuy));
    }

    private function sell()
    {
        $this->stdout('s' . gmp_strval($this->shares));
    }

    private function skip()
    {
        $this->stdout('b0');
    }

    private function stdout($string)
    {
        $stdout = fopen('php://stdout', 'w');
        fputs($stdout, $string);
        fclose($stdout);
    }
}

new StickExchange($argv[1], $argv[2], $argv[3], $argv[4]);

An updated version of "The Greedy Idiot" with re-structured behavior and bug fixes related to working with huge numbers.

Notes:

  • Save in a file and run it like this: php C:\path\path\stack_exchange.php 10 5 100 1
  • This script creates a text file with same name as the script file and a .txt appended to the end. So please run with an user with appropriate write permission on the script path.
  • A simple how to install PHP 7.2 on windows: http://www.dorusomcutean.com/how-to-install-php-7-2-on-windows/
  • To work with super huge numbers, I had to use GMP, so these two lines on php.ini should be un-commented (the semi-colon at start of line should be removed, if it isn't already):
    • ; extension_dir = "ext"
    • ;extension=gmp
\$\endgroup\$
6
  • 1
    \$\begingroup\$ Wow, thanks for that link! I was wondering :D \$\endgroup\$
    – Beta Decay
    Aug 2, 2018 at 20:48
  • 1
    \$\begingroup\$ @BetaDecay: No problem, btw you only need to go until the step 2 (Test PHP is installed) where you check your installation with php -v. The rest aren't needed for this. I believe you are going to have lots of trouble with setting up so many different languages for this challenge! I would never dare to do something like this :D \$\endgroup\$
    – Night2
    Aug 2, 2018 at 20:54
  • \$\begingroup\$ @BetaDecay wouldn't it be easier to just install TryItOnline as a Docker container? \$\endgroup\$
    – Maya
    Aug 3, 2018 at 17:26
  • \$\begingroup\$ @NieDzejkob Possibly but it'll probably come in handy having these languages installed \$\endgroup\$
    – Beta Decay
    Aug 3, 2018 at 17:49
  • 1
    \$\begingroup\$ Congrats, you consistently beat every other contestant! \$\endgroup\$
    – Beta Decay
    Aug 10, 2018 at 15:16
10
\$\begingroup\$

OYAIB

from sys import argv

share_price = float(argv[1])
shares      = int(argv[2])
cash        = float(argv[3])
cur_round   = int(argv[4])
tot_rounds  = 1000.0

investments = shares * share_price
total_assets = investments + cash

target_cash = round(cur_round / tot_rounds * total_assets)

if target_cash > cash:
  shares_to_sell = min(shares, round((target_cash - cash) / share_price))
  print('s%d' % shares_to_sell)
else:
  shares_to_buy = round((cash - target_cash) / share_price)
  print('b%d' % shares_to_buy)

Following the old saying of "own your age in bonds," this program tries to do the same. This way we aren't subject to market volatility at the end-game.

Edit: Looking at the controller it shows that we can only buy/sell full shares, but can have a fractional account balance.

\$\endgroup\$
4
  • \$\begingroup\$ Welcome to PPCG! \$\endgroup\$
    – Beta Decay
    Aug 2, 2018 at 18:28
  • \$\begingroup\$ Thank you! First time posting so let me know if anything is out of place. \$\endgroup\$ Aug 2, 2018 at 18:32
  • \$\begingroup\$ You might want to add an extra condition that on the last round, you sell all of your shares (as investments aren't counted in your score). \$\endgroup\$
    – Riking
    Aug 3, 2018 at 3:01
  • 2
    \$\begingroup\$ That's the beauty of OYAIB, it does that automatically. The target_cash is a percentage of the total_assets depending on what point in "life" it's at. At the end of life, target_cash is 100% of total_assets, so it will sell any shares it's holding. \$\endgroup\$ Aug 3, 2018 at 14:00
8
\$\begingroup\$

Lone Accountant

buy-sell.py:

from sys import argv

Price = int(argv[1])
Shares = int(argv[2])
Balance = float(argv[3])
Round = int(argv[4])

if Round % 2 == 0: print('s' + str(Shares))
if Round % 2 == 1: print('b' + str(int((Balance + 1000) / Price)))

Doesn't store anything in buy-sell.txt.

In odd rounds, it buys as many shares as it can. In even rounds, it sells all of its shares.

The intent is to first raise the share price by buying as many shares as possible, and then sell those shares to get more money. It works because the final round is even (round 1000).

Even though the share price will remain the same (5) after each pair of rounds (assuming the bot is alone, therefore Lone Accountant), the bot's balance increases, since the selling price is higher than the buying price, and more balance leads to the ability of buying more shares. It's a vicious cycle, but in a good way (for me).

The major vulnerability comes with evil bots playing alongside, selling in order to lower the share price (not sure if it's good for them either). In this case, the bot may remain with a balance of $-890, provided there are enough evil bots. This accountant really wants their peace of mind. ;-)

\$\endgroup\$
6
  • \$\begingroup\$ 1 on 1 I'm not sure if beating this is possible; it isn't easy even if you fully understand the accountant LA and try to counter it. In a mass game where you are outnumbered you could be outmaneuvered. \$\endgroup\$
    – Yakk
    Aug 2, 2018 at 19:19
  • \$\begingroup\$ @Yakk Others have already beaten this in my test runs. \$\endgroup\$ Aug 2, 2018 at 19:20
  • \$\begingroup\$ 1 on 1? I am puzzled; I am unable to work out how an opponent you can get rich enough to invert the price swings, or even prevent them from growing in magnitude over time without burning a pile of resources (meanwhile LA doesn't make the sacrifice, so becomes harder to stop). Can you link to the gameplay that LA lost one-on-one? \$\endgroup\$
    – Yakk
    Aug 2, 2018 at 19:24
  • \$\begingroup\$ @Yakk I haven't tested it one-on-one yet. Also, there's a chat room for us to discuss it if you want. \$\endgroup\$ Aug 2, 2018 at 19:34
  • \$\begingroup\$ Would it be more robust to do nothing if you have shares and the price is lower than the previous round or you have money and the price is higher? It would guard against being out of sync with other, similar bots. Also, I don't see how this could be beaten one-on-one. \$\endgroup\$
    – JollyJoker
    Aug 3, 2018 at 8:38
5
\$\begingroup\$

The Passive Trader

from sys import argv

share_price = int(argv[1])
balance = float(argv[3])
round_num = int(argv[4])

if round_num == 1:
    print('b%s' % str(int(balance / share_price)))
else:
    print('b0')

This guy isn't big on this whole "Stocks" thing, but he heard that if he just spends a little money now, he'll get little bits of money over time that'll add up to more than he spent.

He'll buy enough stocks to go to $0 (no overdraft for this fella, he's not gunna put himself in debt for a little profit), then sit around letting the dividends build

Run with python3, but should(?) work with python2 as well.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ I think you should sell your 15 stocks in the last round at least. \$\endgroup\$ Aug 2, 2018 at 15:52
  • 15
    \$\begingroup\$ @Kaldo nah, he's long forgotten about that one time he bought stocks by that point \$\endgroup\$
    – Mayube
    Aug 2, 2018 at 15:57
4
\$\begingroup\$

The Naïve Statistician

Made for Python 3, might work in Python 2

from sys import argv
from math import floor

# Save an entry to the stock history
def save_history(price):
    with open('stockhistory.txt', 'a') as f:
        f.write(str(price) + '\n')

# Load the stock history
def load_history():
    with open('stockhistory.txt', 'r') as f:
        return [float(line.strip()) for line in f]

# Calculate average price rise/fall streak length
def average_streak(history, condition):
    streaks = []
    current_streak = 0
    last_price = history[0]
    for price in history[1:]:
        if condition(last_price, price):
            current_streak += 1
        elif current_streak:
            streaks += [current_streak]
            current_streak = 0
        last_price = price
    if current_streak:
        streaks += [current_streak]
    return sum(streaks) / len(streaks) if streaks else None

# Calculate the current streak length
def current_streak(history, condition):
    streak = 0
    while streak < len(history) - 1 and condition(history[-streak - 2], history[-streak - 1]):
        streak += 1
    return streak

def run(share_price, share_count, balance, round_number):
    save_history(share_price)

    # Sell all shares if it is the last round
    if round_number == 1000:
        print('s' + str(int(share_count)))
        return

    # Buy as many shares as possible if the price is down to one, as there's
    # nothing to lose
    if share_price == 1:
        buy_count = int(balance + 1000)
        print('b' + str(buy_count))
        return

    history = load_history()

    # Calculate the average and current rise/fall streaks
    average_rise = average_streak(history, lambda a, b: a <= b)
    current_rise = current_streak(history, lambda a, b: a <= b)
    average_fall = average_streak(history, lambda a, b: a >= b)
    current_fall = current_streak(history, lambda a, b: a >= b)

    # Do nothing if there's no analyzed data
    if not average_fall or not average_rise:
        print('b0')
        return

    # Buy shares if the current rise streak is as long as or longer than average
    if current_rise > current_fall and current_rise >= average_rise:
        buy_count = (balance + 1000) / share_price
        print('b' + str(int(buy_count)))
        return

    # Sell shares if the current fall streak is as long as or longer than average
    if current_fall > current_rise and current_fall >= average_fall:
        print('s' + str(int(share_count)))
        return

    # Otherwise, do nothing    
    print('b0')

run(*map(float, argv[1:]))

This is a naïve statistician that tries to predict stock prices by only buying/selling if the price has risen/fallen for longer than usual, while also buying stocks if the price is down to one and selling all stocks on the last round.

\$\endgroup\$
0
4
\$\begingroup\$

Percentage Trader Python3

(maybe works in python2)

import sys
args=sys.argv

price=int(args[1])
held=int(args[2])
money=int(args[3])
roundNum=int(args[4])
prevPrice=0

if roundNum==1:
    print("b"+str((money+1000)//price))
else:
    if roundNum==1000:
        print("s"+str(held))
    else:
        with open("percentageTrader.dat","r") as f:
            prevPrice=int(f.read())
        if(price>prevPrice):
            toSell=int(held*int(1000000*(price-prevPrice))/(price))//1000000
            print("s"+str(toSell))
        if(price<prevPrice):
            toBuy=int(((money+1000)//price)*int(1000000*(prevPrice-price))//(prevPrice))//1000000
            print("b"+str(toBuy))
        if(price==prevPrice):
            print("b0")

with open("percentageTrader.dat","w") as f:
    f.write(str(price))

Instructions on running

  • Save as filename.py
  • Run with python filename.py price #shares balance round#

How it works

  • First round the bot purchases as many shares as it can afford.
  • If the price increases, the bot sells a percentage of shares equal to the percentage increase in price(calculated from new value)
  • If the price decreases, the bot buys a percentage of the maximum shares it could buy equal to the percentage decrease in price(calculated from previous value)
  • Sells off everything at round 1000

Changes should hopefully remove the problems caused by floating point division

\$\endgroup\$
4
\$\begingroup\$

Equalizer

from sys import argv
p, n, b, r = map(int, argv[1:])
c = p*n
print "bs"[(c+b)/2>b] + str(int(abs(((c-b)/2)/p))) if r < 999.5 else "s" + str(int(n))

Partitions its financial resources equally between cash and stocks on every round except the last. I believe this strategy to be a mathematically sound way of making at least some money, but I may be proven wrong.

There may or not be bugs that I haven't caught. Also golfed somewhat.

\$\endgroup\$
2
  • \$\begingroup\$ Your program is having difficulties with the large numbers involved here, so I'd suggest changing the line p, n, b, r = map(float, argv[1:]) into p, n, b, r = map(int, argv[1:]) \$\endgroup\$
    – Beta Decay
    Aug 6, 2018 at 16:49
  • \$\begingroup\$ @BetaDecay done \$\endgroup\$
    – praosylen
    Aug 6, 2018 at 22:50
4
\$\begingroup\$

Monkeys On A Typewriter

import random

cmd = ['b', 's'][int(random.random() * 2)]
num = str(int(random.random() * 1000000))
print("%s%s" % (cmd, num))

It's a bunch of monkeys on typewriters. Randomly sells or buys X stocks, where:
0 <= X <= 1,000,000

Run with python3, but should(?) work with python2 as well

\$\endgroup\$
6
  • 4
    \$\begingroup\$ Why not use cmd=random.choose(['b','s']) and num = str(random.randint(0, 1000000))? \$\endgroup\$
    – Beta Decay
    Aug 2, 2018 at 17:08
  • 1
    \$\begingroup\$ Because I'm lazy \$\endgroup\$
    – Mayube
    Aug 2, 2018 at 23:02
  • 1
    \$\begingroup\$ why not just import lazy \$\endgroup\$
    – Woohoojin
    Aug 3, 2018 at 12:06
  • \$\begingroup\$ the whole thing could be reduced to from random import randint, choice;print("{}{}".format(choice(["b", "s"]), randint(0, 1e6))) ;-P \$\endgroup\$
    – Aaron F
    Aug 3, 2018 at 13:24
  • 6
    \$\begingroup\$ yes, but this isn't a golf challenge \$\endgroup\$
    – Mayube
    Aug 3, 2018 at 19:27
4
\$\begingroup\$

Buy Low

(Python 2 or 3)

import random

def run(price, shares, balance, round_):
    # We get no value from our leftover shares at the end, so sell them all.
    if round_ == 1000:
        print('s' + str(int(shares)))
        return

    # If the price is low enough, buy everything we can.
    if price <= 20 + round_ * 60:
        print('b' + str((balance + 1000) // price))
        return

    # If we have no shares, wait for the price to drop.
    if shares == 0:
        print('b0')
        return

    # Sometimes sell shares so we can buy if the price gets low again.
    if random.random() < 0.4:
        print('s1')
        return

    # Otherwise, just wait for a better price.
    print('b0')


if __name__ == '__main__':
    import sys
    run(*[float(x) for x in sys.argv[1:]])
\$\endgroup\$
0
3
\$\begingroup\$

Fallacious Gambler

(Python 2 or 3)

import random

def run(price, shares, balance, round_):
    # We get no value from our leftover shares at the end, so sell them all.
    if round_ == 1000:
        print('s' + str(int(shares)))
        return

    # For the first round, just watch.
    if round_ == 1:
        with open('fg.txt', 'w') as f:
            f.write('1 0 10')
        print('b0')
        return

    # Get the state.
    with open('fg.txt') as f:
        direction, streak, previous = map(int, f.read().strip().split())
    change = price - previous

    # If the market isn't moving, wait for it to get hot again.
    if change == 0:
        print('b0')
        return

    # Keep track of the market direction.
    if (change > 0) == (direction > 0):
        streak += 1
    else:
        streak = 0
        direction *= -1

    # If the market's been going one way for too long, it has to switch, right?
    if streak > 5:
        if direction > 0:
            print('s' + str(shares // 2))
        else:
            print('b' + str((balance + 1000) // price // 2))
    # Otherwise, the market's too volatile.
    else:
        print('b0')

    # Save the state.
    with open('fg.txt', 'w') as f:
        f.write('%d %d %d' % (direction, streak, price))


if __name__ == '__main__':
    import sys
    run(*[float(x) for x in sys.argv[1:]])
\$\endgroup\$
0
3
\$\begingroup\$

The Dollar Cost Averager

(tested with Python 3.7)

First post in codegolf so tell me if I did something wrong.

The basic idea is to buy one share each round if that's possible and sell all shares at the end.

from sys import argv
share_price = int(argv[1])
share_count = int(argv[2])
balance = float(argv[3])
round = int(argv[4])

if round < 1000:
    if balance > share_price-1000:
        print("b1")
    else:
        print("b0")
else:
    print("s" + str(share_count))
\$\endgroup\$
3
\$\begingroup\$

The (Dyalog) APL Farmer

r←apl_stock_farmer args
 round←¯1↑args
 :If 1=round
     (buyPrice sellPrice)←10 0
     bought←1
     (currPrice shares balance)←3↑args
     r←'b10'
 :ElseIf 1000=round
     r←'s',⍕shares
 :Else
     (currPrice shares balance)←3↑args
     :If (currPrice>buyPrice)∧bought
         bought←0
         sellPrice←currPrice
         r←'s',⍕shares
     :ElseIf (currPrice<sellPrice)∧~bought
         bought←1
         buyPrice←currPrice
         r←'b',⍕⌊(1000+balance)÷currPrice
     :Else
         r←'b0'
     :End
 :End

A TradFn which buys every possible share on the first round, and only sells when the current price of the shares is higher than the price they were bought for. After selling, the bot will only buy shares that are cheaper than the price it last sold shares for.

That's because the farmer's accountant told him that's how you do stock trading. "Buy low, sell high" and all that stuff.

Disclaimer

This is my first attempt at a KotH challenge, and since I basically only do APL here, I decided to go on with it.

That said, I'm not completely sure if this will be able to be run alongside the other bots, since it's a Tradfn and can't be fed directly into a CMD/Bash shell.

So, to run this in Bash, you need the following command:

$ echo apl_stock_farmer args | dyalog 'stock_exchange.dws' -script

Where:

apl_stock_farmer is the name of the function, which is in the first line of code.

args is a vector of space separated arguments (in the first round, it would be 10 5 100 1).

dyalog is the path to the Dyalog executable

'stock_exchange.dws' is the name (or path, if the file is not in the same directory the shell has open) of the workspace that contains the function. That workspace file can be obtained by opening a clear workspace, typing )ed apl_stock_farmer, pasting the code above and then doing a )save <path>. I can also provide this workspace file if that would be easier.

-script is just an argument that makes dyalog execute the given code and print to stdout without actually opening the REPL.

Unfortunately, I haven't found a way to make it work with the Windows CMD or Powershell, so I ran it using Git Bash. I'm not sure how feasible it is to put this bot on the competition, but I like this code way too much not to post it.

\$\endgroup\$
2
  • \$\begingroup\$ Sorry, I only have the unregistered version of Dyalog APL, so I'm not sure that this will work as an entrant to the competition \$\endgroup\$
    – Beta Decay
    Aug 6, 2018 at 11:16
  • \$\begingroup\$ @BetaDecay I understand, no problems there. I also found out you can use the Pynapl library to run this code. The details are under "Accessing APL from Python", specifically "Defining a tradfn using Python", and it looks pretty straightforward. \$\endgroup\$
    – J. Sallé
    Aug 6, 2018 at 12:46
3
\$\begingroup\$

Illiterate Dividend Investor

import random
from sys import argv

price = float(argv[1])
shares = int(argv[2])
cash = float(argv[3])
round = int(argv[4])

# buy 1st round, sell last round
if round == 1:
    print('b' + str(int((cash + 1000) / price)))
elif round == 1000:
    print('s' + str(shares))

# round right before dividend: sell
elif round % 5 == 4:
    print('s' + str(shares))

# 1 round after dividend: buy
elif round % 5 == 0:
    print('b' + str(int((cash + 1000) / price)))

# 2 rounds after dividend: 50/50 sell/try to buy
elif round % 5 == 1:
    if random.random() < 0.5:
        print('s' + str(shares))
    else:
        print('b' + str(int((cash + 1000) / price)))

# 3 rounds after dividend: sell if own shares (didn't sell last round), else buy
elif round % 5 == 2:
    if shares > 0:
        print('s' + str(shares))
    else:
        print('b' + str(int((cash + 1000) / price)))

# otherwise, 4 rounds after dividend, buy
else:
    print('b' + str(int((cash + 1000) / price)))

Assumes after dividends, people have more cash, so they will be more likely to buy. Sells right before dividends, buys right after. Goes through one other sell/buy cycle in the other 3 rounds.

\$\endgroup\$
7
  • \$\begingroup\$ Looking at the controller, the dividends pay out every round after the 4th, not only every 5th round. Your cycle should still work but probably not as you intended. \$\endgroup\$
    – Veskah
    Aug 3, 2018 at 21:38
  • \$\begingroup\$ If you buy after other people buy you end up buying when more expensive. \$\endgroup\$ Aug 3, 2018 at 21:40
  • \$\begingroup\$ Thanks @Veskah. Had to add in some r1/r1000 logic as well. \$\endgroup\$
    – brian_t
    Aug 3, 2018 at 21:50
  • \$\begingroup\$ @fəˈnɛtɪk - assuming people buy the round after dividends, you too would want to buy that round and then sell afterward, no? \$\endgroup\$
    – brian_t
    Aug 3, 2018 at 21:51
  • \$\begingroup\$ There is also no round after dividends as you get dividends every round after the 4th. \$\endgroup\$ Aug 3, 2018 at 21:54
2
\$\begingroup\$

Novice Broker (but gets the basic idea)

se_stock_exchange.rb:

DATA_FILE = $0.sub /\.rb$/, ".data"
NUM_ROUNDS = 1000

share_price, num_shares, money, round = ARGV.map &:to_i

order = "s0"

if round == NUM_ROUNDS
  puts "s#{num_shares}"
  exit
end

if File.exists? DATA_FILE
  last_price, trend, bought_price = File.read(DATA_FILE).lines.map &:to_i
else
  last_price = 0
  trend = -1
  bought_price = 0
end

if (new_trend = share_price <=> last_price) != trend
  case trend
  when -1
    order = "b#{(money + 1000) / share_price}"
    bought_price = [bought_price, share_price].max
  when 1
    if share_price > bought_price
      order = "s#{num_shares}"
      bought_price = 0
    end
  end
  trend = new_trend
end

File.open(DATA_FILE, "w") { |f| f.puts share_price, trend, bought_price }

puts order

Waits until the price turns around, then buys/sells everything. I mean, that's what it says to do in Day Trading for Dummies Note: this is probably a real book, and that's probably something someone might get from it.

Saves data in se_stock_exchange.data. Run with ruby se_stock_exchange.rb ${SHARE_PRICE} ${SHARES} ${MONEY} ${ROUND} (substituting the appropriate values).

\$\endgroup\$
6
  • \$\begingroup\$ This is my first stab at KotH, so let me know if I'm doing it all wrong. \$\endgroup\$ Aug 2, 2018 at 16:27
  • 2
    \$\begingroup\$ Nice: amazon.co.uk/Day-Trading-Dummies-Ann-Logue/dp/047094272X \$\endgroup\$
    – Beta Decay
    Aug 2, 2018 at 17:04
  • \$\begingroup\$ I get this error: se_stock_exchange.rb:24:in `<main>': undefined method `+' for nil:NilClass (NoMethodError) \$\endgroup\$ Aug 2, 2018 at 17:31
  • 4
    \$\begingroup\$ @BetaDecay: Too bad the author's middle name doesn't start with an 'A'. \$\endgroup\$
    – 3D1T0R
    Aug 2, 2018 at 18:16
  • 3
    \$\begingroup\$ @NieDzejkob: If it were an 'A': "Ann A. Logue" is analogous to "Analog". \$\endgroup\$
    – 3D1T0R
    Aug 2, 2018 at 23:02
2
\$\begingroup\$

Half More or Nothing

def run(price, shares, balance, cur_round):
    if cur_round==1000:
        print('s'+str(int(shares)))
        return
    
    if cur_round==1:
        with open('HalfMoreOrNothing.dat', 'w') as f:
            f.write(str(int(price)))
        print('b'+str(int((balance+1000)/price)))
        return
    
    if shares==0:
        with open('HalfMoreOrNothing.dat', 'w') as f:
            f.write(str(int(price)))
        print('b'+str(int((balance+1000)/price)))
        return
    
    with open('HalfMoreOrNothing.dat', 'r') as f:
        bought_price=int(f.read())
    if price>=bought_price*1.5:
        print('s'+str(int(shares)))
        return
    
    print('b0')

if __name__ == '__main__':
    import sys
    run(*[float(x) for x in sys.argv[1:]])

I rarely use Python, let me know if this generates an error somewhere.

The strategy is to wait until the share price is at least 50% bigger than the price at the moment of bying them, then sell them, then immediately buy new shares so it can wait for the new share price raise.

Hopefully, people chimps won't start to sell shares near the end... (it seems that most bots just wait for the right moment, whatever that is)

\$\endgroup\$
2
\$\begingroup\$

Buy/Reinvest as much as possible!

Similar to my Dollar-Cost Averager that did, surprisingly, quite average, this buys each round as many shares as are affordable and only sells them in the last round.

from sys import argv

share_price = int(argv[1])
share_count = int(argv[2])
balance = float(argv[3])
round = int(argv[4])


if round < 1000:
    if balance > share_price-1000:
        buy_count = int((balance+1000)/share_price)
        print("b"+str(buy_count))
    else:
        print("b0")
else:
    print("s" + str(share_count))
\$\endgroup\$
2
  • \$\begingroup\$ Hey, you've got an error with your indentation here. Did you mean to indent the if balance > share_price-1000: block or not? \$\endgroup\$
    – Beta Decay
    Aug 6, 2018 at 12:15
  • \$\begingroup\$ Yes. My minor edit seems to have disrupted my formatting. Will fix as soon as I'mback on a pc \$\endgroup\$ Aug 6, 2018 at 18:27
2
\$\begingroup\$

Fibonacci

I've rewritten this in Python 3 to make things easier. Hopefully!

import math
from sys import argv

price = float(argv[1])
shares = int(argv[2])
balance = float(argv[3])
roundNum = int(argv[4])

fibonacci = [2,3,5,8,13,21,34,55,89,144,233,377,610,987]
if (roundNum == 1):
    buy = int((balance+1000)/price)
    print('b' + str(buy))
elif (roundNum in fibonacci) and roundNum % 2 == 1 and balance > 0:
    buy = int((balance/price)/2)
    print('b' + str(buy))
elif ((roundNum in fibonacci) and roundNum % 2 == 0) or roundNum % 100 == 0:
    if (roundNum == 1000):
        sell = shares
        print('s' + str(sell))
    else:
        sell = math.ceil(shares/2)
        print('s' + str(sell))
else:
    print('b0')

It buys half the maximum amount of shares that is affordable when the round is equal to an odd Fibonacci number and sells half of the available shares when the round is equal to an even Fibonacci number and also every 100 rounds. Sells all shares on round 1000. Otherwise, it just waits. Only buys shares when balance is positive.

\$\endgroup\$
2
  • \$\begingroup\$ Hey, I get the error Error in roundNum%%2 : non-numeric argument to binary operator Execution halted \$\endgroup\$
    – Beta Decay
    Aug 6, 2018 at 11:37
  • \$\begingroup\$ @BetaDecay I updated the code which may fix the issue. Let me know. \$\endgroup\$
    – Robert S.
    Aug 6, 2018 at 14:30
2
\$\begingroup\$

Technical Analysis Robot

I study business economics so I tried to realize the simpliest method for analizing a stock market (the technical analysis). According to the theory you just have to analyze all the minimums of the graph to see if there is a trend (up ord down). During an up trend you have to buy and during a down trend you have to sell.

I don't think that this method will work too well but let's give it a try :)

import sys
from sys import argv

share_price = int(argv[1])
share_number = int(argv[2])
bank_account = float(argv[3])
round_number = int(argv[4])

max_buy_greedily = (1000 + bank_account) / share_price
minima = []

def log():
    f = open("log_technical_analysis.txt","a+")
    f.write("%d;" % share_price)

def analyze():
    f = open("log_technical_analysis.txt","r+")
    line = f.readline()
    values = line.split(";")
    values.pop()
    for i in range(len(values) - 1):
        if i > 0 and int(values[i-1]) > int(values[i]) and int(values[i+1]) > int(values[i]):
            minima.append(int(values[i]))
    if len(minima) >= 3 and minima[len(minima) - 1] > minima[len(minima) - 2] and minima[len(minima) - 2] > minima[len(minima) - 3]:
        print('b' + str(int(max_buy_greedily)))
    elif len(minima) >= 3 and minima[len(minima) - 1] < minima[len(minima) - 2] and minima[len(minima) - 2] < minima[len(minima) - 3]:
        print('s' + str(share_number))
    else:
        print('b0')

if round_number >= 994:
    print('s' + str(share_number))
    sys.exit(0)

if share_price <= 15:
    print('b' + str(int(max_buy_greedily)))
    log()
    sys.exit(0)

log()
analyze()
sys.exit(0)

Tested with python3

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Good luck! This is far from a normal market :D \$\endgroup\$
    – Beta Decay
    Aug 7, 2018 at 20:47
  • 1
    \$\begingroup\$ @BetaDecay haha yeah :] but you would be wondering how random most of the people spend their money in the stock market (or bitcoin) :D \$\endgroup\$
    – Solenya
    Aug 7, 2018 at 20:52
1
\$\begingroup\$

Greedy B*****d

# Gready one...
from sys import argv

SMA_PERIOD = 5
LAST_BUY_DAY = 985
LAST_SELL_DAY = 993

# Save an entry to the stock history
def save_history(price):
    with open('db.txt', 'a') as f:
        f.write(str(price) + '\n')

# Load the stock history
def load_history():
    with open('db.txt', 'r') as f:
        return [float(line.strip()) for line in f]

def get_sma(d, n):
    l = d[-n:]
    return int(sum(l) / len(l))


def buy(price, account):
    if account + 1000 > 0:
        print 'b' + str(int((account + 1000) / price))
        return
    print 'b0'

def sell(holdings):
    print 's'+ str(int(holdings))


def run(price, holdings, account, day):

    save_history(price)
    d = load_history()

    if price <= get_sma(d, SMA_PERIOD) and day < LAST_BUY_DAY:
        return buy(price, account)

    if price > get_sma(d, SMA_PERIOD):
        return sell(holdings)

    if day >= LAST_SELL_DAY:
        return sell(holdings)

    # Otherwise, do nothing    
    print 'b0'


run(*map(float, argv[1:]))  

He will go all in when its cheap and sell it all once the price goes up...

\$\endgroup\$
8
  • \$\begingroup\$ Your code is all over the place. Firstly, you return print statements but also you pass three arguments to sell() which only takes one \$\endgroup\$
    – Beta Decay
    Aug 6, 2018 at 14:48
  • \$\begingroup\$ Typo with three args to sell()... now whats your concern with returning print statements? \$\endgroup\$
    – Arek S
    Aug 7, 2018 at 6:44
  • \$\begingroup\$ Just that they’re unnecessary \$\endgroup\$
    – Beta Decay
    Aug 7, 2018 at 10:25
  • \$\begingroup\$ some argue they help with readability \$\endgroup\$
    – Arek S
    Aug 7, 2018 at 10:30
  • \$\begingroup\$ you didnt include it in results because of prints? afaik typo in sale() definition wont stop it working... I fix that by the way \$\endgroup\$
    – Arek S
    Aug 7, 2018 at 10:32
1
\$\begingroup\$

Lucky Number 6

EDIT: Oh ffs, I think me not converting the selling count to int was one of my problems, well here we go again.

Probably my last contribution, unless I'm bored at work and make something a bit more sophisticated, but I fell like the sophisticated bots already fill the niches.

This guy basically sells some of his shares every 6 rounds, because hey 6 is his lucky number.

from sys import argv
import random

share_price = int(argv[1])
share_count = int(argv[2])
balance = float(argv[3])
round = int(argv[4])
x = random.uniform(1,2)

if round == 1 or round == 1000:
    print("s"+str(share_count))
elif round % 6 == 0 and share_price >= 10:
    sell = int(share_count/x)
    print("s"+str(sell))
elif balance > share_price-1000:
    buy_count = int((balance+1000)/share_price)
    print("b"+str(buy_count))
else:
    print("b0")
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.