├── .gitignore ├── lib ├── degiro.rb └── degiro │ ├── version.rb │ ├── errors.rb │ ├── user_data.rb │ ├── urls_map.rb │ ├── find_products.rb │ ├── get_cash_funds.rb │ ├── get_portfolio.rb │ ├── find_product_by_id.rb │ ├── get_orders.rb │ ├── get_transactions.rb │ ├── connection.rb │ ├── client.rb │ └── create_order.rb ├── Gemfile ├── .rubocop.yml ├── degiro.gemspec ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | *.gem 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /lib/degiro.rb: -------------------------------------------------------------------------------- 1 | require_relative 'degiro/client.rb' 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/degiro/version.rb: -------------------------------------------------------------------------------- 1 | module DeGiro 2 | VERSION = '0.0.2'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/degiro/errors.rb: -------------------------------------------------------------------------------- 1 | module DeGiro 2 | class MissingSessionIdError < StandardError; end 3 | 4 | class MissingUrlError < StandardError; end 5 | class IncorrectUrlError < StandardError; end 6 | 7 | class MissingUserFieldError < StandardError; end 8 | class IncorrectUserFieldError < StandardError; end 9 | end 10 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - bin/* 4 | 5 | Metrics/LineLength: 6 | Max: 130 7 | 8 | Metrics/MethodLength: 9 | Max: 30 10 | 11 | Metrics/AbcSize: 12 | Enabled: false 13 | 14 | Naming/AccessorMethodName: 15 | Enabled: false 16 | 17 | Style/Next: 18 | Enabled: false 19 | 20 | Style/WordArray: 21 | Enabled: false 22 | 23 | Style/NumericLiterals: 24 | Enabled: false 25 | 26 | Documentation: 27 | Enabled: false 28 | -------------------------------------------------------------------------------- /lib/degiro/user_data.rb: -------------------------------------------------------------------------------- 1 | require_relative 'errors.rb' 2 | 3 | module DeGiro 4 | class UserData 5 | USER_FIELDS = [ 6 | 'id', 7 | 'intAccount' 8 | ].freeze 9 | 10 | def initialize(data) 11 | @map = USER_FIELDS.each_with_object({}) do |user_field, acc| 12 | raise MissingUserFieldError, "Could not find user field '#{user_field}'" unless data.key?(user_field) 13 | acc[user_field.gsub(/(.)([A-Z])/, '\1_\2').downcase] = data[user_field] 14 | end 15 | end 16 | 17 | def [](user_field) 18 | raise IncorrectUserFieldError, "Could not find user field '#{user_field}'" unless @map.key?(user_field) 19 | @map[user_field] 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/degiro/urls_map.rb: -------------------------------------------------------------------------------- 1 | require_relative 'errors.rb' 2 | 3 | module DeGiro 4 | class UrlsMap 5 | URL_NAMES = [ 6 | 'paUrl', 7 | 'productSearchUrl', 8 | 'productTypesUrl', 9 | 'reportingUrl', 10 | 'tradingUrl', 11 | 'vwdQuotecastServiceUrl' 12 | ].freeze 13 | 14 | def initialize(data) 15 | @map = URL_NAMES.each_with_object({}) do |url_name, acc| 16 | raise MissingUrlError, "Could not find url '#{url_name}'" unless data.key?(url_name) 17 | acc[url_name.gsub(/(.)([A-Z])/, '\1_\2').downcase] = data[url_name] 18 | end 19 | end 20 | 21 | def [](url_name) 22 | raise IncorrectUrlError, "Could not find url '#{url_name}'" unless @map.key?(url_name) 23 | @map[url_name] 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /degiro.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'degiro/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'degiro' 7 | spec.homepage = 'https://github.com/vaneyckt/degiro' 8 | spec.licenses = ['MIT'] 9 | spec.version = DeGiro::VERSION 10 | spec.description = 'Ruby Client for the unofficial DeGiro API' 11 | spec.summary = 'Ruby Client for the unofficial DeGiro API' 12 | spec.authors = ['Tom Van Eyck'] 13 | spec.email = ['tomvaneyck@gmail.com'] 14 | 15 | spec.files = Dir.glob('lib/**/*.rb') 16 | spec.require_paths = ['lib'] 17 | 18 | spec.add_dependency 'faraday', '~> 0.13.1' 19 | spec.add_dependency 'faraday-cookie_jar', '~> 0.0.6' 20 | 21 | spec.add_development_dependency 'rubocop', '~> 0.51.0' 22 | end 23 | -------------------------------------------------------------------------------- /lib/degiro/find_products.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module DeGiro 4 | class FindProducts 5 | def initialize(connection) 6 | @connection = connection 7 | end 8 | 9 | def find_products(search_text:, limit: 7) 10 | params = URI.encode_www_form(searchText: search_text, limit: limit) 11 | parse_products(JSON.parse(@connection.get(url(params)).body)) 12 | end 13 | 14 | private 15 | 16 | def parse_products(response) 17 | response['products'].map do |product| 18 | { 19 | id: product['id'].to_s, 20 | ticker: product['symbol'], 21 | exchange_id: product['exchangeId'], 22 | isin: product['isin'] 23 | } 24 | end 25 | end 26 | 27 | def url(params) 28 | "#{@connection.urls_map['product_search_url']}/v5/products/lookup" \ 29 | "?intAccount=#{@connection.user_data['int_account']}" \ 30 | "&sessionId=#{@connection.session_id}" \ 31 | "&#{params}" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/degiro/get_cash_funds.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module DeGiro 4 | class GetCashFunds 5 | def initialize(connection) 6 | @connection = connection 7 | end 8 | 9 | def get_cash_funds 10 | params = URI.encode_www_form(cashFunds: 0) 11 | parse_cash_funds(JSON.parse(@connection.get(url(params)).body)) 12 | end 13 | 14 | private 15 | 16 | def parse_cash_funds(response) 17 | funds = response['cashFunds']['value'].map do |cash| 18 | { 19 | currency: cash['value'].find { |field| field['name'] == 'currencyCode' }['value'], 20 | amount: cash['value'].find { |field| field['name'] == 'value' }['value'] 21 | } 22 | end 23 | Hash[funds.map { |cash| [cash[:currency], cash[:amount]] }] 24 | end 25 | 26 | def url(params) 27 | "#{@connection.urls_map['trading_url']}/v5/update/" \ 28 | "#{@connection.user_data['int_account']};jsessionid=#{@connection.session_id}" \ 29 | "?#{params}" 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeGiro 2 | 3 | A simple Ruby client for [DeGiro](https://www.degiro.co.uk/). Inspired by this [javascript client](https://github.com/pladaria/degiro). 4 | 5 | DeGiro's API is likely to change in the future and I make no guarantees that I'll be making changes to keep the client up-to-date. Use this software at your own risk! 6 | 7 | ## How to use 8 | 9 | Install the gem with `gem install degiro`. Supported commands are shown below. 10 | 11 | ``` 12 | require 'degiro' 13 | 14 | client = DeGiro::Client.new(login: 'my_login', password: 'my_password') 15 | 16 | client.get_orders 17 | client.get_portfolio 18 | client.get_cash_funds 19 | client.get_transactions 20 | 21 | id = client.find_products(search_text: 'GOOG').first[:id] 22 | client.find_product_by_id(id: id) 23 | 24 | client.create_market_buy_order(product_id: id, size: 10) 25 | client.create_market_sell_order(product_id: id, size: 10) 26 | client.create_limit_buy_order(product_id: id, size: 10, price: 1000) 27 | client.create_limit_sell_order(product_id: id, size: 10, price: 1000) 28 | ``` 29 | -------------------------------------------------------------------------------- /lib/degiro/get_portfolio.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module DeGiro 4 | class GetPortfolio 5 | def initialize(connection) 6 | @connection = connection 7 | end 8 | 9 | def get_portfolio 10 | params = URI.encode_www_form(portfolio: 0) 11 | parse_portfolio(JSON.parse(@connection.get(url(params)).body)) 12 | end 13 | 14 | private 15 | 16 | def parse_portfolio(response) 17 | portfolio = response['portfolio']['value'].map do |order| 18 | { 19 | size: order['value'].find { |field| field['name'] == 'size' }['value'], 20 | value: order['value'].find { |field| field['name'] == 'price' }['value'], 21 | product_id: order['value'].find { |field| field['name'] == 'id' }['value'].to_s 22 | } 23 | end 24 | portfolio.select { |entry| entry[:size] > 0 } 25 | end 26 | 27 | def url(params) 28 | "#{@connection.urls_map['trading_url']}/v5/update/" \ 29 | "#{@connection.user_data['int_account']};jsessionid=#{@connection.session_id}" \ 30 | "?#{params}" 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/degiro/find_product_by_id.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module DeGiro 4 | class FindProductById 5 | def initialize(connection) 6 | @connection = connection 7 | end 8 | 9 | def find_product_by_id(id:) 10 | parse_product(JSON.parse(find_by_id(id).body)) 11 | end 12 | 13 | private 14 | 15 | def find_by_id(product_id) 16 | @connection.post(url) do |req| 17 | req.headers['Content-Type'] = 'application/json; charset=UTF-8' 18 | req.body = [product_id].to_json 19 | end 20 | end 21 | 22 | def parse_product(response) 23 | { 24 | id: response['data'].values[0]['id'].to_s, 25 | ticker: response['data'].values[0]['symbol'].to_s, 26 | exchange_id: response['data'].values[0]['exchangeId'].to_s, 27 | isin: response['data'].values[0]['isin'].to_s 28 | } 29 | end 30 | 31 | def url 32 | "#{@connection.urls_map['product_search_url']}/v5/products/info" \ 33 | "?intAccount=#{@connection.user_data['int_account']}" \ 34 | "&sessionId=#{@connection.session_id}" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tom Van Eyck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/degiro/get_orders.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module DeGiro 4 | class GetOrders 5 | def initialize(connection) 6 | @connection = connection 7 | end 8 | 9 | def get_orders 10 | params = URI.encode_www_form(orders: 0, historicalOrders: 0, transactions: 0) 11 | parse_orders(JSON.parse(@connection.get(url(params)).body)) 12 | end 13 | 14 | private 15 | 16 | def parse_orders(response) 17 | response['orders']['value'].map do |order| 18 | { 19 | id: order['id'], 20 | type: order['value'].find { |field| field['name'] == 'buysell' }['value'], 21 | size: order['value'].find { |field| field['name'] == 'size' }['value'], 22 | price: order['value'].find { |field| field['name'] == 'price' }['value'], 23 | product_id: order['value'].find { |field| field['name'] == 'productId' }['value'].to_s, 24 | product: order['value'].find { |field| field['name'] == 'product' }['value'] 25 | } 26 | end 27 | end 28 | 29 | def url(params) 30 | "#{@connection.urls_map['trading_url']}/v5/update/" \ 31 | "#{@connection.user_data['int_account']};jsessionid=#{@connection.session_id}" \ 32 | "?#{params}" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/degiro/get_transactions.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | require 'json' 3 | 4 | module DeGiro 5 | class GetTransactions 6 | def initialize(connection) 7 | @connection = connection 8 | end 9 | 10 | def get_transactions(from: (Date.today - (365 * 5)).strftime('%d/%m/%Y'), to: Date.today.strftime('%d/%m/%Y')) 11 | params = URI.encode_www_form(fromDate: from, toDate: to) 12 | parse_transactions(JSON.parse(@connection.get(url(params)).body)) 13 | end 14 | 15 | private 16 | 17 | def parse_transactions(response) 18 | response['data'] 19 | .sort_by { |transaction| transaction['date'] } 20 | .reverse 21 | .map do |transaction| 22 | { 23 | id: transaction['id'], 24 | type: transaction['buysell'], 25 | size: transaction['quantity'].abs, 26 | price: transaction['price'], 27 | product_id: transaction['productId'].to_s 28 | } 29 | end 30 | end 31 | 32 | def url(params) 33 | "#{@connection.urls_map['reporting_url']}/v4/transactions" \ 34 | '?orderId=&product=&groupTransactionsByOrder=false' \ 35 | "&intAccount=#{@connection.user_data['int_account']}" \ 36 | "&sessionId=#{@connection.session_id}" \ 37 | "&#{params}" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/degiro/connection.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'faraday' 3 | require 'faraday-cookie_jar' 4 | 5 | require_relative 'urls_map.rb' 6 | require_relative 'user_data.rb' 7 | require_relative 'errors.rb' 8 | 9 | module DeGiro 10 | class Connection 11 | extend Forwardable 12 | 13 | def_delegators :@conn, :get, :post 14 | attr_reader :urls_map, :user_data, :session_id 15 | 16 | BASE_TRADER_URL = 'https://trader.degiro.nl'.freeze 17 | 18 | def initialize(login, password) 19 | @conn = Faraday.new(url: BASE_TRADER_URL) do |builder| 20 | builder.use :cookie_jar 21 | builder.use Faraday::Response::RaiseError 22 | builder.adapter Faraday.default_adapter 23 | end 24 | 25 | response = @conn.post('/login/secure/login') do |req| 26 | req.headers['Content-Type'] = 'application/json' 27 | req.body = { 28 | username: login, 29 | password: password, 30 | isPassCodeReset: false, 31 | isRedirectToMobile: false, 32 | queryParams: { reason: 'session_expired' } 33 | }.to_json 34 | end 35 | 36 | @session_id = response.headers['set-cookie'][/JSESSIONID=(.*?);/m, 1] 37 | raise MissingSessionIdError, 'Could not find valid session id' if @session_id == '' || @session_id.nil? 38 | @urls_map = UrlsMap.new(JSON.parse(@conn.get('/login/secure/config').body)["data"]) 39 | @user_data = UserData.new(JSON.parse(@conn.get("#{@urls_map['pa_url']}/client?sessionId=#{@session_id}").body)['data']) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/degiro/client.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | require_relative 'connection.rb' 4 | require_relative 'create_order.rb' 5 | require_relative 'find_product_by_id.rb' 6 | require_relative 'find_products.rb' 7 | require_relative 'get_cash_funds.rb' 8 | require_relative 'get_orders.rb' 9 | require_relative 'get_portfolio.rb' 10 | require_relative 'get_transactions.rb' 11 | 12 | module DeGiro 13 | class Client 14 | extend Forwardable 15 | 16 | def_delegators :@create_order, :create_market_buy_order 17 | def_delegators :@create_order, :create_market_sell_order 18 | def_delegators :@create_order, :create_limit_buy_order 19 | def_delegators :@create_order, :create_limit_sell_order 20 | def_delegators :@find_product_by_id, :find_product_by_id 21 | def_delegators :@find_products, :find_products 22 | def_delegators :@get_cash_funds, :get_cash_funds 23 | def_delegators :@get_orders, :get_orders 24 | def_delegators :@get_portfolio, :get_portfolio 25 | def_delegators :@get_transactions, :get_transactions 26 | 27 | def initialize(login:, password:) 28 | connection = Connection.new(login, password) 29 | 30 | @create_order = CreateOrder.new(connection) 31 | @find_product_by_id = FindProductById.new(connection) 32 | @find_products = FindProducts.new(connection) 33 | @get_cash_funds = GetCashFunds.new(connection) 34 | @get_orders = GetOrders.new(connection) 35 | @get_portfolio = GetPortfolio.new(connection) 36 | @get_transactions = GetTransactions.new(connection) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/degiro/create_order.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module DeGiro 4 | class CreateOrder 5 | BUY_SELL = { buy: 'BUY', sell: 'SELL' }.freeze 6 | ORDER_TYPES = { limited: 0, stop_limited: 1, market: 2, stop_loss: 3 }.freeze 7 | TIME_TYPES = { day: 1, permanent: 3 }.freeze 8 | 9 | def initialize(connection) 10 | @connection = connection 11 | end 12 | 13 | def create_market_buy_order(product_id:, size:) 14 | order = market_order(BUY_SELL[:buy], product_id, size) 15 | confirmation_id = JSON.parse(check_order(order).body)['data']['confirmationId'] 16 | confirm_order(order, confirmation_id) 17 | end 18 | 19 | def create_market_sell_order(product_id:, size:) 20 | order = market_order(BUY_SELL[:sell], product_id, size) 21 | confirmation_id = JSON.parse(check_order(order).body)['data']['confirmationId'] 22 | confirm_order(order, confirmation_id) 23 | end 24 | 25 | def create_limit_buy_order(product_id:, size:, price:) 26 | order = limit_order(BUY_SELL[:buy], product_id, size, price) 27 | confirmation_id = JSON.parse(check_order(order).body)['data']['confirmationId'] 28 | confirm_order(order, confirmation_id) 29 | end 30 | 31 | def create_limit_sell_order(product_id:, size:, price:) 32 | order = limit_order(BUY_SELL[:sell], product_id, size, price) 33 | confirmation_id = JSON.parse(check_order(order).body)['data']['confirmationId'] 34 | confirm_order(order, confirmation_id) 35 | end 36 | 37 | private 38 | 39 | def market_order(type, product_id, size) 40 | { 41 | buySell: type, 42 | orderType: ORDER_TYPES[:market], 43 | productId: product_id, 44 | size: size, 45 | timeType: TIME_TYPES[:permanent] 46 | } 47 | end 48 | 49 | def limit_order(type, product_id, size, price) 50 | { 51 | buySell: type, 52 | orderType: ORDER_TYPES[:limited], 53 | productId: product_id, 54 | size: size, 55 | timeType: TIME_TYPES[:permanent], 56 | price: price 57 | } 58 | end 59 | 60 | def check_order(order) 61 | @connection.post(check_order_url) do |req| 62 | req.headers['Content-Type'] = 'application/json; charset=UTF-8' 63 | req.body = order.to_json 64 | end 65 | end 66 | 67 | def confirm_order(order, confirmation_id) 68 | @connection.post(confirm_order_url(confirmation_id)) do |req| 69 | req.headers['Content-Type'] = 'application/json; charset=UTF-8' 70 | req.body = order.to_json 71 | end 72 | end 73 | 74 | def check_order_url 75 | "#{@connection.urls_map['trading_url']}/v5/checkOrder" \ 76 | ";jsessionid=#{@connection.session_id}" \ 77 | "?intAccount=#{@connection.user_data['int_account']}" \ 78 | "&sessionId=#{@connection.session_id}" 79 | end 80 | 81 | def confirm_order_url(confirmation_id) 82 | "#{@connection.urls_map['trading_url']}/v5/order/#{confirmation_id}" \ 83 | ";jsessionid=#{@connection.session_id}" \ 84 | "?intAccount=#{@connection.user_data['int_account']}" \ 85 | "&sessionId=#{@connection.session_id}" 86 | end 87 | end 88 | end 89 | --------------------------------------------------------------------------------