Собаки 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, то на них и были реализованы тестовые раннеры:
'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,
},
]);
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?
Автор: pronskiy