finance.lisp 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. (in-package #:chatikbot)
  2. (defparameter +yahoo-url+ "https://query.yahooapis.com/v1/public/yql" "Yahoo Finance API endpoint")
  3. (defparameter +yahoo-query+ "select Name,Rate from yahoo.finance.xchange where pair in (~{\"~A\"~^,~})")
  4. (defparameter +brent-url+ "http://www.cmegroup.com/CmeWS/mvc/Quotes/Future/424/G")
  5. (defvar *rate-pairs* '("USDRUB" "EURRUB" "GBPRUB"))
  6. (defparameter +chart-ranges+ (list (cons "day" (* 24 60))
  7. (cons "week" (* 7 24 60))
  8. (cons "month" (* 30 24 60))
  9. (cons "quarter" (* 91 24 60))
  10. (cons "year" (* 365 24 60))))
  11. (defun get-rates (&optional (pairs *rate-pairs*))
  12. (let ((response (json-request
  13. +yahoo-url+
  14. :parameters (append '(("format" . "json")
  15. ("env" . "store://datatables.org/alltableswithkeys"))
  16. (list (cons "q" (format nil +yahoo-query+ pairs)))))))
  17. (when (aget "error" response)
  18. (error "Error in rates request"))
  19. (loop for rate in (aget "rate" (aget "results" (aget "query" response)))
  20. collect (cons (aget "Name" rate)
  21. (read-from-string (aget "Rate" rate))))))
  22. (defun get-brent ()
  23. (handler-case
  24. (let ((last (read-from-string
  25. (aget "last" (first (aget "quotes" (json-request +brent-url+)))))))
  26. (when (numberp last)
  27. last))
  28. (error (e) (log:error e))))
  29. (defparameter +btc-e-usd-url+ "https://btc-e.com/api/2/btc_usd/ticker" "BTC-e BTC/USD ticker url")
  30. (defun get-btc-e ()
  31. (handler-case
  32. (aget "last" (aget "ticker" (json-request +btc-e-usd-url+)))
  33. (error (e) (log:error e))))
  34. (defparameter +coindesk-btc-usd-url+ "https://api.coindesk.com/site/headerdata.json?currency=BTC" "coindesk.com api endpoint")
  35. (defun get-coindesk-btc-usd ()
  36. (handler-case
  37. (agets (json-request +coindesk-btc-usd-url+)
  38. "bpi" "USD" "rate_float")
  39. (error (e) (log:error "~A" e))))
  40. (defun get-serie (series idx)
  41. (loop for row in series
  42. when (elt row idx)
  43. collect (list (first row) (elt row idx))))
  44. (defun range (seq &key (key 'identity))
  45. (loop
  46. for item in seq
  47. for value = (funcall key item)
  48. minimizing value into min
  49. maximizing value into max
  50. finally (return (values min max))))
  51. (defvar *chart-points* 800 "Data points in chart")
  52. (defun make-chart (series names)
  53. (let* ((r (multiple-value-list (range series :key #'car)))
  54. (fmt (if (<= (- (second r) (first r))
  55. (* 60 60 36))
  56. '((:hour 2) ":" (:min 2))
  57. '((:day 2) "." (:month 2) " " (:hour 2) ":" (:min 2)))))
  58. (adw-charting:with-line-chart ((+ 90 *chart-points*)
  59. (floor (* 3 *chart-points*) 4))
  60. (loop for serie in names
  61. for idx from 1
  62. do (adw-charting:add-series serie (get-serie series idx)))
  63. (adw-charting:set-axis
  64. :x "Time" :draw-gridlines-p t
  65. :label-formatter #'(lambda (v)
  66. (local-time:format-timestring
  67. nil
  68. (local-time:unix-to-timestamp v)
  69. :format fmt)))
  70. (adw-charting:set-axis :y "RUB" :draw-gridlines-p t :label-formatter "~,2F")
  71. (adw-charting:save-file "chart.png"))))
  72. ;; Database
  73. (def-db-init
  74. (db-execute "create table if not exists finance (ts, usd, eur, gbp, brent, btc)")
  75. (db-execute "create index if not exists fin_ts_ids on finance (ts)"))
  76. (defun db/add-finance (ts usd eur gbp brent btc)
  77. (db-execute "insert into finance (ts, usd, eur, gbp, brent, btc) values (?, ?, ?, ?, ?, ?)"
  78. ts usd eur gbp brent btc))
  79. (defun db/get-last-finance ()
  80. (values-list (first (db-select "select ts, usd, eur, gbp, brent, btc from finance order by ts desc limit 1"))))
  81. (defparameter +finance-db-map+ '(("usd" . "USD/RUB")
  82. ("eur" . "EUR/RUB")
  83. ("gbp" . "GBP/RUB")
  84. ("brent" . "Brent")
  85. ("btc" . "BTC/USD")))
  86. (defun db/get-series (after-ts &optional (fields '("usd" "eur" "gbp" "brent")) (avg 60))
  87. (when fields
  88. (let ((sql (format nil
  89. "select ts/~a*~:*~a,~{avg(~a) as ~:*~a~^,~} from finance where ts >= ? group by ts/~a order by ts"
  90. avg fields avg)))
  91. (values
  92. (db-select sql (local-time:timestamp-to-unix after-ts))
  93. (sublis +finance-db-map+ fields :test #'equal)))))
  94. ;; Cron
  95. (defcron process-rates ()
  96. (let ((ts (local-time:timestamp-to-unix (local-time:now)))
  97. (rates (get-rates))
  98. (brent (get-brent))
  99. (btc (get-coindesk-btc-usd)))
  100. (db/add-finance ts (aget "USD/RUB" rates) (aget "EUR/RUB" rates) (aget "GBP/RUB" rates) brent btc)))
  101. ;;; Hooks
  102. (def-message-cmd-handler handler-rates (:rates)
  103. (multiple-value-bind (ts usd eur gbp brent btc) (db/get-last-finance)
  104. (bot-send-message chat-id
  105. (format nil "Зеленый *~,2F*, гейро *~,2F*, британец *~,2F*, чёрная *~,2F*, btc *~,2F* @ _~A_"
  106. usd eur gbp brent btc
  107. (format-ts (local-time:unix-to-timestamp ts)))
  108. :parse-mode "Markdown")))
  109. (def-message-cmd-handler handler-charts (:charts)
  110. (telegram-send-chat-action chat-id "upload_photo")
  111. (let* ((args (mapcar 'string-downcase args))
  112. (all-fields (mapcar #'car +finance-db-map+))
  113. (fields (or (intersection args all-fields :test 'equal) all-fields))
  114. (day-range (some #'(lambda (a) (aget a +chart-ranges+)) args))
  115. (number (some #'(lambda (a) (parse-integer a :junk-allowed t)) args))
  116. (avg (* 60 (cond
  117. (day-range (round day-range *chart-points*))
  118. (number)
  119. (:otherwise 1))))
  120. (after-ts (local-time:timestamp- (local-time:now)
  121. (* avg *chart-points*) :sec))
  122. (rates (multiple-value-list (db/get-last-finance)))
  123. (chart (apply #'make-chart (multiple-value-list
  124. (db/get-series after-ts fields avg)))))
  125. (telegram-send-photo chat-id chart
  126. :caption
  127. (format nil "Зеленый ~,2F, гейро ~,2F, британец ~,2F, чёрная ~,2F, btc ~,2F @ ~A"
  128. (elt rates 1) (elt rates 2) (elt rates 3) (elt rates 4) (elt rates 5)
  129. (format-ts (local-time:unix-to-timestamp (elt rates 0)))))))