...

вторник, 13 марта 2018 г.

dev||bet – битва программистов и технологий

Собаки vs кошки, водители vs пешеходы, Coca-Cola vs Pepsi, BMW vs Mercedes, колбаса vs сыр, узвар vs морс, добро vs зло, в конце концов! Но нет, мы, как всегда, спорили о том, какой язык программирования лучше. В ход шли классические аргументы о производительности со ссылками на бенчмарки, которые никто не проверял, синтаксические плюшки, которые используешь раз в год, графики популярности, списки авторитетных программистов использующих тот или иной язык… Затем разговор плавно перешел в обсуждение баттла Оксимирона с кем-то там. Ну и любой разговор дольше 20 минут сводится к обсуждению цены на биткоин.
Странным образом три темы слились в одну и так родилась идея dev||bet.

Суть проекта


Два разработчика, представляющие разные технологии, решают одну и ту же задачу. Побеждает тот, кто решит ее в кратчайшее время, или получит лучше результат за ограниченное время.
Трое комментаторов наблюдают за процессом на экранах участников. Они обсуждают их подход к решению, подмечают интересные моменты, нестандартные ходы и просто интересные фишки, которые есть у любого программиста. А с другой стороны, обращают внимание на неудачные решения и провалы.
Мы, зрители, хотим увидеть как обычные разработчики думают, гуглят, ошибаются, и в, конце концов, приходят к решению. Более того, нам скорее интереснее наблюдать за затыком лидов над библиотечной функцией, чем щелканье алгоритмов натасканным олимпиадником.

Для пилотного выпуска в качестве участников мы позвали друзей, которые и определили используемые технологии: Python vs JavaScript.

Задача первого выпуска


Чтобы проект был успешным в 2018 году, в его названии обязательно должно быть упоминание криптовалют или блокчейн. Поэтому немного помечтав, мы придумали несложную задачу. Формальное описание, которое получали участники выглядело вот так:
Сегодня все говорят о криптовалютах. Было много случаев, когда люди продавали биткоины по невероятно низкой цене. Ну а я когда-то думал купить 150 BTC за $15…
Но что, если бы у нас была машина времени, которая могла бы передавать команды крипто брокеру в прошлое? Конечно, мы бы обрушили глобальную финансовую систему. Тем не менее давайте представим, что машина у нас есть, ну или, по крайней мере, мы близки к ее созданию. Мы хотим, чтобы ты, грязный голодный фрилансер работающий за еду, создал для нас алгоритм, который генерирует последовательность команд для нашего брокер-клиента. (Которого мы сами создали год назад специально для использования в будущем, конечно же.)
Ты можешь использовать Python или JavaScript. Пожалуйста, проверь свое решение с помощью приложенного тестового раннера. Тот чье решение заработает больше денег, будет выбран, чтобы написать бота для реального клиента.

API specification v0.0.18


Раннер задачи должен быть чистой функцией, которая принимает два аргумента:
prices => [{"btc": 1000}, ...] – Массив цен на биткоин по дням
initialSum => 100000 – Стартовый баланс в USD
Функция должна возвращать массив команд. Команды вызываются по очереди, одна в день. То есть для 14 дней у вас должно быть ровно 14 команд.
Пример:
[{"op":"buy","amount":1},{"op":"sell","amount":1}]
Команды:
"buy" дополнительные атрибуты: amount [float] – Купить BTC используя текущий USD счет
"sell" дополнительные атрибуты: amount [float] – Продать BTC используя текущий USD счет
"pass" – Пропустить день

Описание задачи намеренно было сделано запутанным и нечетким, чтобы сделать процесс интереснее. Правда, в результате это сыграло злую шутку, и сильно увеличило время решения участниками.
По сути же требовалось написать функцию, которая по уже известной истории изменения цен выдаст инструкции когда стоит покупать биткоины, когда продавать, ну а когда просто подождать.

Реализация


В качестве платформы была выбрана площадка Codewars. Не то чтоб мы долго выбирали, но на ней было все необходимое: поддержка более 20 языков, простой интерфейс, возможность добавить черновик задачи доступный по ссылке.
Довольно интересным кажется проект codebattle.hexlet.io, о котором уже писали на Хабре. Но возможность видеть код соперника пока нам показалась лишней.

Поскольку участники использовали Python и JavaScript, то на них и были реализованы тестовые раннеры:

JavaScript
'use strict';

const https = require('https');

const currencies = [
    'btc',
    'eth',
];
const BASE_CURRENCY = 'usd';
const DEFAULT_CURRENCY = 'btc';

const fetchRates = (days, currency) => new Promise((res, rej) => {
    https.get(`https://min-api.cryptocompare.com/data/histoday?aggregate=1&e=CCCAGG&extraParams=CryptoCompare&limit=${days}&tryConversion=false&tsym=${BASE_CURRENCY.toUpperCase()}&fsym=${currency.toUpperCase()}`, (resp) => {
        let data = '';
        resp.on('data', (chunk) => {
            data += chunk;
        });
        resp.on('end', () => {
            data = JSON.parse(data);
            data = data.Data;

            res(data.map(datum => datum['close']).slice(0, -1));
        });

    }).on("error", err => rej(err));
});

const fetchAllRates = async (days, currencies) => {
  const prices = {};
  for (let currency of currencies) {
      prices[currency] = await fetchRates(days, currency);
  }

  const len = prices[Object.keys(prices)[0]].length;
  const ret = [];
  for (let i = 0; i < len; i++) {
      let price = {};
      for (let currency of currencies) {
          price[currency] = prices[currency][i];
      }
      ret.push(price);
  }

  return ret;
};

const checkStash = stash => {
    const vals = Object.values(stash);
    for (let val of vals) {
        Test.expect(val >= -Math.pow(10, -6), 'Invalid operation');
        if (val < -Math.pow(10, -6)) {
            throw new Error(`Debts are not supported. Stash: ${JSON.stringify(stash)}`)
        }
    }
};

const applyTask = (stash, task, prices) => {
    console.log('- performing task', stash, task, prices);
    const currency = task.currency || DEFAULT_CURRENCY;

    switch(task.op) {
        case 'buy':
            stash[currency] += task.amount;
            stash[BASE_CURRENCY] -= task.amount * prices[currency];
            break;
        case 'sell':
            stash[currency] -= task.amount;
            stash[BASE_CURRENCY] += task.amount * prices[currency];
            break;
        case 'pass':
            break;
    }

    return stash;
};

const runner = async (trader, cases) => {
    for (let testCase of cases) {
        let prices = await fetchAllRates(testCase.days, currencies);

        let stash = testCase.amount;
        for (let currency of currencies) {
            stash[currency] = stash[currency] || 0;
        }

        console.log(`Testing amount ${stash[BASE_CURRENCY]}, days ${testCase.days}`);
        let tasks = await trader(prices, stash[BASE_CURRENCY]);
        for (let i in tasks) {
            if (!tasks.hasOwnProperty(i)) {
                continue;
            }
            let job = tasks[i];
            let todo = job.length ? job : [job];
            for (let row of todo) {
                await applyTask(stash, row, prices[i]);
            }
            checkStash(stash);
        }

        let result = Math.floor(stash[BASE_CURRENCY] * 100) / 100;
        console.log(`finished. Resulting amount: ${result}`);
    }
};

runner(trader, [
    {
        amount: {
            [BASE_CURRENCY]: 100,
        },
        days: 100,
    },
]);



Python
import urllib2
import json
import math

currencies = [
    'btc',
    'eth',
]

BASE_CURRENCY = 'usd'
DEFAULT_CURRENCY = 'btc'


def fetch_rates(days, currency):
    data = urllib2.urlopen(
        'https://min-api.cryptocompare.com/data/histoday?aggregate=1&e=CCCAGG&extraParams=CryptoCompare&limit={}&tryConversion=false&tsym={}&fsym={}'.format(
            days, BASE_CURRENCY.upper(), currency.upper())).read()
    data = json.loads(data)['Data']

    return [row['close'] for row in data][:-1]


def fetch_all_rates(days, currencies):
    prices = {currency: fetch_rates(days, currency) for currency in currencies}

    return [{currency: prices[currency][i] for currency in currencies} for i in range(days)]


def check_stash(stash):
    for currency in stash:
        test.assert_equals(stash[currency] >= -0.000001, True, 'Invalid operation')
        if stash[currency] < -0.000001:
            raise Exception('Debts are not supported. Stash: {}'.format(stash))


def apply_task(stash, task, prices):
    print '- performing task {} {} {}'.format(stash, task, prices)
    currency = task['currency'] if 'currency' in task else DEFAULT_CURRENCY

    if task['op'] == 'buy':
        stash[currency] += task['amount']
        stash[BASE_CURRENCY] -= task['amount'] * prices[currency]
    elif task['op'] == 'sell':
        stash[currency] -= task['amount']
        stash[BASE_CURRENCY] += task['amount'] * prices[currency]
    elif task['op'] == 'pass':
        pass

    return stash


def runner(trader, cases):
    for testCase in cases:
        prices = fetch_all_rates(testCase['days'], currencies)

        stash = testCase['amount']
        for currency in currencies:
            if currency not in stash:
                stash[currency] = 0

        print 'Testing amount {}, days {}'.format(stash[BASE_CURRENCY], testCase['days'])
        tasks = trader(prices, stash[BASE_CURRENCY])

        for i, job in enumerate(tasks):
            todo = job if isinstance(job, list) else [job]
            for row in todo:
                stash = apply_task(stash, row, prices[i])
            check_stash(stash)

        result = math.floor(stash[BASE_CURRENCY] * 100) / 100
        print 'finished. Resulting amount: {}'.format(result)


runner(trader, [
    {
        "amount": {
            BASE_CURRENCY: 100,
        },
        "days": 100,
    }
])



Сама задача доступна на Codewars: www.codewars.com/kata/5a22fdffe1ce0e4c5d000031
А решения участников на Github: github.com/devbet/devbet_0

Спасибо за внимание! Ждём ваших комментариев, критики и мнений.
А что же дальше? PHP vs. JS, .Net vs. Java, iOS vs. Android, React vs. Vue.js?

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий