We've made it quicker and easier for your business to access your own real time data, without the need for costly infrastructure or specialist engineers.
Unparalleled flexibility to discover and react to events as they unfold in your data streams, in real time.
Unblock your product roadmap and enable your developers to build compelling new consumer products and internal tools.
Run continuous analytics against your pricing to assess the health of your trading and risk positions.
Connects to your existing APIs.
Ingest and handle your
real time data.
Define the events you want
to take action on and write
Javascript to handle them.
Consumer-grade, intelligent
search over all your markets,
with the latest prices.
Solve problems across product, trading and risk.
Trading receives bets on outcomes with prices that are out-of-date. Customer service now have to cancel the bets and deal with complaints.
Dandelion offers a number of pre-built scripts for monitoring the prices offered through your API.
If these price feeds exhibit unusual behaviour (price jumps, large number of suspensions, lack of updates) then the markets can be flagged with trading or automatically closed through an internal API call.
The business has suffered a lower gross win than normal, but is having trouble identifying the source in a meaningful timeframe.
Dandelion surfaces the mathematics of bookmaking in an easy-to-use framework.
We provide scripts that can automatically track overround, vigorish, implied probabilities and determine market weaknesses. We are even able to detect in game arbitrage opportunities and relay this to your team in near real time.
The product team has an idea for a new application but lacks the engineering resources to prototype it. Engineering are busy for the next 3-6 months.
Dandelion makes it possible to prototype and run new products quickly and easily.
Our programming framework incorporates many of the functions needed to develop new and innovative products. Your developers are able to concentrate on building engaging user experiences, instead of heavy data processing and manipulation.
No need to build your own infrastructure.
Dandelion intelligently polls your existing APIs to construct its own stream of events.
These events are indexed into data storage and then processed through the scripting engine.
All data, processing and scripting is carried out within Dandelion, there is no need for you to provision additional infrastructure.
Once connected, we offer a turnkey solution to your business.
We are also able to offer lower latency integration options by way of push messaging or message queue integration, subject to consultation.
Our scripting engine allows discovery and manipulation of your real time data.
Notify individuals or groups when situations of interest unfold.
Arbitrage, odds conversions and Kelly criterion calculators.
Generate graphs, store logs, use counters, classify and tag data.
Call HTTP services, send e-mails, SMS or post Slack messages.
Scan, query and perform calculations over data that changes over time.
Dynamically create scripts, stop & start scripts, analyze execution metrics.
Per-script and global storage, weighted lists for ranking data
Intelligent search over all markets with the latest pricing information
We provide a programming and execution framework that enables your developers to write scripts that react to changes in your data stream.
Our tooling is delivered through the browser; no need to install additional software or make changes to your corporate infrastructure.
The Dandelion platform handles everything; data ingestion, indexing, event creation, script execution, fast storage and integration capabilities.
Our scripting framework is developer friendly, allowing any developer to write plain old Javascript, in the browser, with the full power of an IDE.
We provide in-browser code completion, making it easy to discover and understand the framework. No lengthy training required.
Documentation and examples are where your developers need it; with the code. Unlock their ability to innovate and deliver.
The framework is built specifically for gaming companies. We provide built-in functions such as: time-windows over prices and probabilities, odds conversions, betting calculations, HTTP integration, notifications and many more.
We provide a common set of capabilities out-of-the-box.
Our approach allows you to get started straight away with these applications,
or consult with us and we can help you craft your own bespoke scripts.
// Vigorish Check Sports.OnMarketUpdated(function(context, market) { var vig = market.Vigorish.Latest; // Check if vigorish can be calculated (i.e. all outcomes are active) if (vig.HasValue == false) { return; } // If less than 5%, send an alert to trading. if (vig.Value <= 5 && vig.Value > 0) { context.Alerting.Group("trading", "Market with low vigorish (" + vig.Value + "%): " + market.GraphName); return; } // If less than 0%, send an alert to trading and call internal API to close market. if (vig.Value <= 0) { context.Alerting.Group("trading", "Market with vigorish under 0% (" + vig.Value + "%): " + market.GraphName); context.Integration.Http.PostFormEncodedNoResponse("https://domain.com/close-market", { marketId: market.Identifier }); } });
// Slow Price Updates Check Sports.OnOutcomePriceChange(function(context, outcome) { // Only select events that are in play. if (outcome.Market.Event.StartedUtc > context.Time.UtcNow) { return; } // Make sure the game has been in play for at least 5 minutes. if (context.Time.SecondsAgo(outcome.Market.Event.StartedUtc) < 300) { return; } // Less than 5 updates in 5 minutes is worthy of an alert. var fiveMinCount = outcome.Market.PriceUpdates.Tail.TakeSeconds(5 * 60).Count; if (fiveMinCount < 5) { context.Alerting.Group("traders", "Low price update count (5 mins: " + fiveMinCount + "): " + outcome.Market.GraphName); } // Less than 1 update in 15 minutes should suspend the outcome if (outcome.Market.PriceUpdates.Tail.TakeSeconds(15 * 60).Count <= 1) { context.Integration.Http.PostFormEncodedNoResponse("https://domain.com/suspend-outcome", { outcomeId: outcome.Identifier }); } });
// Arbitrage Check Sports.OnOutcomePriceChange(function(context, outcome){ // Check if arbitrage exists in this market, if so, alert and close market. if (context.Calculators.Arbitrage.ExistsInOutcomes(outcome.Market.Outcomes)) { context.Alerting.All("Market with built-in arbitrage:" + outcome.Market.GraphName); context.Integration.Http.PostFormEncodedNoResponse("https://domain.com/close-market", { marketId: outcome.Market.Identifier }); } // Select only in play events if (outcome.Market.Event.StartedUtc > context.Time.UtcNow) { return; } // Check if there have been any outcome probability inversions var recent = outcome.Market.OutcomeProbabilityInversions.Tail.TakeSeconds(60); if (recent.Count > 0) { context.Integration.Notifications.Slack( "Potentail arbitrage warning (" + recent.Last.SecondsAgo + " secs ago): " + outcome.Market.GraphName, "#operations"); } });
// Anomalous Prices Check Sports.OnOutcomePriceChange(function(context, outcome){ var alertGroups = ['traders', 'operations']; // Check the last 5 minutes and look for probability swings > 20% var last60seconds = outcome.ImpliedProbability.Tail.TakeSeconds(60); var difference = last60seconds.Maximum - last60seconds.Minimum; if (difference > 20) { context.Alerting.Groups(alertGroups, "Large probability swing in outcome last 60 secs: " + outcome.GraphName); } // Check for the number of suspensions and unsuspensions if (outcome.Suspensions.Tail.TakeSeconds(5 * 60).Count > 5 || outcome.UnSuspensions.Tail.TakeSeconds(5 * 60).Count > 5) { context.Alerting.Groups(alertGroups, "Many un/suspensions in outcome last 60 secs: " + outcome.GraphName); } // Check for the number of outcome inversions if (outcome.Market.OutcomeProbabilityInversions.Tail.TakeSeconds(5 * 60).Count >= 2) { context.Alerting.Groups(alertGroups, "Many outcome inversions in last 5 minutes: " + outcome.GraphName); } });
// Instrumentation - handle market changes Sports.OnMarketUpdated(function(context, market) { if (market.Vigorish.Latest.HasValue) context.Instrumentation.Graphing.Plot("Vigorish", market.Identifier, market.Vigorish.Latest.Value); if (market.Overround.Latest.HasValue) context.Instrumentation.Graphing.Plot("Overound", market.Identifier, market.Overround.Latest.Value); }); // Also handle outcome changes Sports.OnOutcomePriceChange(function(context, outcome) { context.Instrumentation.Counters.Increment(market.Identifier + "-priceChanges"); var last5Mins = outcome.Price.Tail.TakeSeconds(5 * 60); context.Instrumentation.Graphing.Plot("AveragePriceLast5Mins", outcome.Identifier, last5Mins.Average); context.Instrumentation.Graphing.Plot("MaxPriceLast5Mins", outcome.Identifier, last5Mins.Maximum); });
// Price Notifications Sports.OnOutcomePriceChange(function(context, outcome) { var outcomeId = context.Scripts.Current.GetRegistrationParameter("outcomeId"); // Does this outcome match? if (outcome.Identifier == outcomeId) { var price = context.Scripts.Current.GetRegistrationParameter("price"); var direction = context.Scripts.Current.GetRegistrationParameter("direction"); var customerId = context.Scripts.Current.GetRegistrationParameter("customerId"); if (direction == "shorten" && outcome.Price.Latest <= price) { context.Integration.Http.PostJsonNoResponse("https://domain.com/price-shorten", { OutcomeId: outcomeId, CustomerId: customerId, Price: outcome.Price.Latest }); context.Scripts.Current.Delete("Run once."); } else if (direction == "drift" && outcome.Price.Latest >= price) { context.Integration.Http.PostJsonNoResponse("https://domain.com/price-drift", { OutcomeId: outcomeId, CustomerId: customerId, Price: outcome.Price.Latest }); context.Scripts.Current.Delete("Run once."); } } });
// Team Event Notifications Sports.OnEventAdded(function(context, event) { var teamName = context.Scripts.Current.GetRegistrationParameter("teamName"); // Does this event name contain the name of the team? if (event.Name.indexOf(teamName) > -1) { var runOnce = context.Scripts.Current.GetRegistrationParameter("runOnce"); var customerId = context.Scripts.Current.GetRegistrationParameter("customerId"); var customerEmail = context.Scripts.Current.GetRegistrationParameter("customerEmail"); var customerSMS = context.Scripts.Current.GetRegistrationParameter("customerSMS"); var message = "Your team " + teamName + " is playing!"; if (customerEmail.length > 0) context.Integration.Notifications.Email(message, customerEmail); if (customerSMS.length > 0) context.Integration.Notifications.TextMessage(message, customerSMS); if (customerId.length > 0) context.Integration.Http.PostJsonNoResponse("https://domain.com/notify-customer", { Team: teamName, CustomerId: customerId }); if (runOnce) context.Scripts.Current.Delete("Customer elected only one notification."); } });
// Dynamic Promotions Sports.OnOutcomePriceChange(function(context, outcome) { // We only want to apply dynamic promotions to the Australian Tennis Open if (outcome.IsSuspended || outcome.Price.Latest <= 2.5 || outcome.Market.Event.Competition.Sport.Type != SportType.Tennis || outcome.Market.Event.Competition.Name != "Australian Open") { return; } // If the average price over the last 5 minutes is > 4.5 // and the price is now > 6 then apply a price boost. if (outcome.Price.Tail.TakeSeconds(5 * 60).Average >= 4.5 && outcome.Price.Latest > 6) { context.Integration.Http.PostJsonNoResponse("https://domain.com/price-boost-start", { OutcomeId: outcome.Identifier }); } else { context.Integration.Http.PostJsonNoResponse("https://domain.com/price-boost-end", { OutcomeId: outcome.Identifier }); } });
// Sure bets Sports.OnOutcomePriceShorten(function(context, outcome){ // Select only Tennis, 1.5 hours into game, straight markets if (outcome.Market.Event.Competition.Sport.Type != SportType.Tennis && outcome.Market.Event.ElapsedTimeInSeconds <= (1.5 * 60 * 60) && outcome.BetType != BetType.Straight && outcome.Market.Name != "Games Winners") return; // Check the price if (outcome.Price.Latest >= 1.01 && outcome.Price.Latest <= 1.1 && outcome.Price.Tail.TakeSeconds(2 * 60).Average <= 1.1) { var username = context.Scripts.Current.GetRegistrationParameter("username"); var password = context.Scripts.Current.GetRegistrationParameter("password"); var account = context.Accounts.Login(username, password); if (account.Balance.Main < 10){ return; } // Place a bet (if we already haven't) if (account.PendingBets.HasOutcomeBet(outcome) == false) { account.Place(current, 10, false); } } });
// Market Movers function getDifference(timeWindow, secs) { var min = timeWindow.First.Value; var max = timeWindow.First.Value; for (i=0;i++;i<timeWindow.Count) { var item=timeWindow.Values[i]; if (item.SecondsAgo <= secs) { if (item.Value < min) min = item.Value; if (item.Value > max) max = item.Value; } } return max - min; } Sports.OnOutcomePriceChange(function(context, outcome) { var timeWindow = outcome.Price.Tail.TakeSeconds(5 * 60); context.Storage.WeightedLists.Markets.Add(outcome.Market, "5mins", "300s", timeWindow.Maximum - timeWindow.Minimum); context.Storage.WeightedLists.Markets.Add(outcome.Market, "1min", "60s", getDifference(timeWindow, 60)); context.Storage.WeightedLists.Markets.Add(outcome.Market, "30secs", "30s", getDifference(timeWindow, 30)); });
// Random / Conditional Bets // Intercept all outcome events and maintain local storage // This storage can be later queried via an API Sports.OnOutcomeAdded(function(context, outcome) { context.Storage.Local.Put(outcome.Identifier, outcome.Price.Latest); }); Sports.OnOutcomePriceChange(function(context, outcome) { context.Storage.Local.Put(outcome.Identifier, outcome.Price.Latest); }); Sports.OnOutcomeDeleted(function(context, outcome) { context.Storage.Local.Delete(outcome.Identifier); }); Sports.OnOutcomeSuspended(function(context, outcome) { context.Storage.Local.Delete(outcome.Identifier); }); Sports.OnOutcomeUnSuspended(function(context, outcome) { context.Storage.Local.Put(outcome.Identifier, outcome.Price.Latest); });
// Cash-out Profitable Bets Accounts.OnCashoutProfitable(function (context, account, bet, offer) { var profit = offer - bet.Stake; var percentage = profit / bet.Stake; // Cashout only bets that are at least 10% profitable. if (percentage < 0.1) return; // Do we have other bets on this market? var allBetsOnMarket = account.PendingBets.GetMarketBets(bet.Outcome.Market); if (allBetsOnMarket.length == 1) { // Only one bet and it is this one, attempt the cash out var success = account.PendingBets.Cashout(bet, offer); if (success) context.Scripts.Current.Delete("Cashed out only bet"); } else { context.Integration.Notifications.TextMessage("0402123456", "Profitable cashout detected ($" + profit + "), but has multiple bets - " + bet.Identifier); context.Scripts.Current.Stop("Has multiple bets"); } });
// In-Play Arbitrage Sports.OnOutcomePriceChange(function(context, outcome) { // Check this is a market we are interested in if (outcome.Market.IsHeadToHead == false || outcome.Market.Outcomes.length > 2 || outcome.Market.OutcomeProbabilityInversions.Count < 1) return; // Assuming the market has oscillated once in the last 2 mins and // Price is over 3, we make a bet. if (outcome.Market.OutcomeProbabilityInversions.Tail.TakeSeconds(2 * 60).Count > 1 && outcome.Price.Latest > 3) { var account = context.Accounts.Login("username","password"); if (account.PendingBets.HasOutcomeBet(outcome) == false) account.Place(outcome, 10, false); } }); // Second part is to pick off the arbitrage bets at 25% profit Accounts.OnArbitrage(function(context, account, bet, outcome, suggested, profit){ if (profit > bet.Stake * 1.25) account.Place(outcome, suggested, false); else context.Integration.Notifications.TextMessage("Arb detected on: " + bet.Identifier, "0420123456"); });
Built with
usingGet in touch to start a conversation.
Based in Hobart; Our engineers work around the world.
We are looking for passionate and competent software engineers, get in touch.
No recruiters please.
© 2024 Divide Data
All rights reserved.