Skip to content

Instantly share code, notes, and snippets.

@anatomic
Last active June 28, 2016 11:52
Show Gist options
  • Save anatomic/fb58dc98840ab59d25bd3f9bf8ba128e to your computer and use it in GitHub Desktop.
Save anatomic/fb58dc98840ab59d25bd3f9bf8ba128e to your computer and use it in GitHub Desktop.
const { __, pick, compose, curry, concat, map, reduce, cond,
T, divide, multiply, add, subtract, lift, view, lensProp, lensPath, over,
prop, props, identity, findLast, keys, sort, split, apply } = require("ramda");
const Either = require('data.either');
const SCORECAST_CONVERSIONS = {
'1': '1/2000',
'1.001': '1/1000',
'1.002': '1/400',
'1.003': '1/300',
'1.004': '1/250',
'1.005': '1/200',
'1.007': '1/150',
'1.008': '1/125',
'1.01': '1/100',
'1.012': '1/80',
'1.015': '1/66',
'1.017': '1/60',
'1.02': '1/50',
'1.025': '1/40',
'1.03': '1/33',
'1.033': '1/30',
'1.036': '1/28',
'1.04': '1/25',
'1.046': '1/22',
'1.05': '1/20',
'1.053': '1/19',
'1.056': '1/18',
'1.062': '1/16',
'1.067': '1/15',
'1.071': '1/14',
'1.077': '1/13',
'1.083': '1/12',
'1.091': '1/11',
'1.1': '1/10',
'1.111': '1/9',
'1.118': '2/17',
'1.125': '1/8',
'1.133': '2/15',
'1.143': '1/7',
'1.154': '2/13',
'1.167': '1/6',
'1.182': '2/11',
'1.2': '1/5',
'1.222': '2/9',
'1.25': '1/4',
'1.286': '2/7',
'1.3': '30/100',
'1.333': '1/3',
'1.35': '7/20',
'1.364': '4/11',
'1.4': '2/5',
'1.444': '4/9',
'1.45': '9/20',
'1.471': '40/85',
'1.5': '1/2',
'1.533': '8/15',
'1.571': '4/7',
'1.6': '3/5',
'1.615': '8/13',
'1.625': '5/8',
'1.667': '4/6',
'1.7': '7/10',
'1.727': '8/11',
'1.8': '4/5',
'1.833': '5/6',
'1.9': '9/10',
'1.909': '10/11',
'1.952': '20/21',
'2': '1/1',
'2.05': '21/20',
'2.1': '11/10',
'2.2': '6/5',
'2.25': '5/4',
'2.3': '13/10',
'2.375': '11/8',
'2.4': '7/5',
'2.5': '6/4',
'2.6': '8/5',
'2.625': '13/8',
'2.7': '17/10',
'2.75': '7/4',
'2.8': '9/5',
'2.875': '15/8',
'2.9': '19/10',
'3': '2/1',
'3.1': '21/10',
'3.125': '85/40',
'3.2': '11/5',
'3.25': '9/4',
'3.3': '23/10',
'3.375': '95/40',
'3.4': '12/5',
'3.5': '5/2',
'3.6': '13/5',
'3.75': '11/4',
'3.8': '14/5',
'4': '3/1',
'4.2': '16/5',
'4.333': '100/30',
'4.5': '7/2',
'4.6': '18/5',
'5': '4/1',
'5.5': '9/2',
'6': '5/1',
'6.5': '11/2',
'7': '6/1',
'7.5': '13/2',
'8': '7/1',
'8.5': '15/2',
'9': '8/1',
'9.5': '17/2',
'10': '9/1',
'11': '10/1',
'12': '11/1',
'13': '12/1',
'14': '13/1',
'15': '14/1',
'16': '15/1',
'17': '16/1',
'18': '17/1',
'19': '18/1',
'20': '19/1',
'21': '20/1',
'23': '22/1',
'26': '25/1',
'29': '28/1',
'31': '30/1',
'34': '33/1',
'36': '35/1',
'41': '40/1',
'46': '45/1',
'51': '50/1',
'56': '55/1',
'61': '60/1',
'67': '66/1',
'71': '70/1',
'76': '75/1',
'81': '80/1',
'86': '85/1',
'91': '90/1',
'96': '95/1',
'101': '100/1',
'106': '105/1',
'111': '110/1',
'116': '115/1',
'121': '120/1',
'126': '125/1',
'131': '130/1',
'136': '135/1',
'141': '140/1',
'146': '145/1',
'151': '150/1',
'156': '155/1',
'161': '160/1',
'166': '165/1',
'171': '170/1',
'176': '175/1',
'181': '180/1',
'186': '185/1',
'191': '190/1',
'196': '195/1',
'201': '200/1',
'211': '210/1',
'221': '220/1',
'231': '230/1',
'241': '240/1',
'251': '250/1',
'261': '260/1',
'271': '270/1',
'281': '280/1',
'291': '290/1',
'301': '300/1',
'326': '325/1',
'351': '350/1',
'376': '375/1',
'401': '400/1',
'426': '425/1',
'451': '450/1',
'476': '475/1',
'501': '500/1',
'551': '550/1',
'601': '600/1',
'651': '650/1',
'701': '700/1',
'751': '750/1',
'801': '800/1',
'851': '850/1',
'901': '900/1',
'951': '950/1',
'1001': '1000/1',
'1101': '1100/1',
'1201': '1200/1',
'1251': '1250/1',
'1501': '1500/1',
'1601': '1600/1',
'1701': '1700/1',
'1801': '1800/1',
'1901': '1900/1',
'2001': '2000/1',
'2501': '2500/1',
'3001': '3000/1',
'4001': '4000/1',
'5001': '5000/1',
'6001': '6000/1',
'7001': '7000/1',
'8001': '8000/1',
'9001': '9000/1',
'10001': '10000/1'
};
const buildOrderedKeys = compose(sort(subtract), map((p) => parseFloat(p)),keys);
const ORDERED_KEYS = buildOrderedKeys(SCORECAST_CONVERSIONS);
const scorer = {
price: {
num: 5,
den: 1
},
type: "home"
}
const correctScore = {
score: {
home: 1,
away: 0
},
price: {
num: 13,
den: 2
},
type: "home"
}
/**
* Check if the selection is actually possible
*/
const validateSelectedOutcomes = (scorer, correctScore) => correctScore.score[scorer.type] > 0 ?
Either.Right( [scorer, correctScore] ) :
Either.Left("Invalid selection");
/**
* Work out what the price is based on type of the outcomes and overall score
*/
const isDraw = ([scorer, correctScore]) => correctScore.type === "draw";
const scorerAndTeamMatch = ([ scorer, correctScore ]) => scorer.type === correctScore.type;
const calculatePriceWithScaleFactor = (factor) => compose(reduce(multiply, factor), map(prop('price')))
const calculateTruePrice = cond(
[
[isDraw, calculatePriceWithScaleFactor(0.8)],
[scorerAndTeamMatch, calculatePriceWithScaleFactor(0.6)],
[T, calculatePriceWithScaleFactor(1.25)]
]
);
/**
* Convert the outcomes' prices to decimals and scale appropriately
*/
const convertToDecimal = over(lensProp('price'), compose(add(1), apply(divide), props([ 'num', 'den' ])));
const buildPrice = compose(calculateTruePrice, map(convertToDecimal));
/**
* Take the true price and identify the nearest neighbour from the original price map
*/
const pickNearestPriceFromMap = (keys, map) => (decimalPrice) => prop(findLast((p) => parseFloat(p) < decimalPrice, keys), map)
const findNearestPrice = pickNearestPriceFromMap(ORDERED_KEYS, SCORECAST_CONVERSIONS);
/**
* Convert a string representation of a price to a Price object
*/
const toPriceObject = compose(([num, den]) => ({num,den}), split('/'))
const getPrice = compose(toPriceObject, findNearestPrice, buildPrice)
const app = compose(map(getPrice), validateSelectedOutcomes)
// Price should be 25/1
console.log('---');
console.log(app(scorer, correctScore));
console.log('---');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment