finance.lisp 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  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. (defun get-serie (series idx)
  35. (loop for row in series
  36. when (elt row idx)
  37. collect (list (first row) (elt row idx))))
  38. (defun range (seq &key (key 'identity))
  39. (loop
  40. for item in seq
  41. for value = (funcall key item)
  42. minimizing value into min
  43. maximizing value into max
  44. finally (return (values min max))))
  45. (defvar *chart-points* 800 "Data points in chart")
  46. (defun make-chart (series names)
  47. (let* ((r (multiple-value-list (range series :key #'car)))
  48. (fmt (if (<= (- (second r) (first r))
  49. (* 60 60 36))
  50. '((:hour 2) ":" (:min 2))
  51. '((:day 2) "." (:month 2) " " (:hour 2) ":" (:min 2)))))
  52. (adw-charting:with-line-chart ((+ 90 *chart-points*)
  53. (floor (* 3 *chart-points*) 4))
  54. (loop for serie in names
  55. for idx from 1
  56. do (adw-charting:add-series serie (get-serie series idx)))
  57. (adw-charting:set-axis
  58. :x "Time" :draw-gridlines-p t
  59. :label-formatter #'(lambda (v)
  60. (local-time:format-timestring
  61. nil
  62. (local-time:unix-to-timestamp v)
  63. :format fmt)))
  64. (adw-charting:set-axis :y "RUB" :draw-gridlines-p t :label-formatter "~,2F")
  65. (adw-charting:save-file "chart.png"))))
  66. ;; Database
  67. (def-db-init (db)
  68. (%db-execute db "create table if not exists finance (ts, usd, eur, gbp, brent, btc)")
  69. (%db-execute db "create index if not exists fin_ts_ids on finance (ts)"))
  70. (defun db/add-finance (ts usd eur gbp brent btc)
  71. (db-execute "insert into finance (ts, usd, eur, gbp, brent, btc) values (?, ?, ?, ?, ?, ?)"
  72. ts usd eur gbp brent btc))
  73. (defun db/get-last-finance ()
  74. (values-list (first (db-select "select ts, usd, eur, gbp, brent, btc from finance order by ts desc limit 1"))))
  75. (defparameter +finance-db-map+ '(("usd" . "USD/RUB")
  76. ("eur" . "EUR/RUB")
  77. ("gbp" . "GBP/RUB")
  78. ("brent" . "Brent")
  79. ("btc" . "BTC/USD")))
  80. (defun db/get-series (after-ts &optional (fields '("usd" "eur" "gbp" "brent")) (avg 60))
  81. (when fields
  82. (let ((sql (format nil
  83. "select ts/~a*~:*~a,~{avg(~a) as ~:*~a~^,~} from finance where ts >= ? group by ts/~a order by ts"
  84. avg fields avg)))
  85. (values
  86. (db-select sql (local-time:timestamp-to-unix after-ts))
  87. (sublis +finance-db-map+ fields :test #'equal)))))
  88. ;; Cron
  89. (defcron process-rates ()
  90. (let ((ts (local-time:timestamp-to-unix (local-time:now)))
  91. (rates (get-rates))
  92. (brent (get-brent))
  93. (btc (get-btc-e)))
  94. (db/add-finance ts (aget "USD/RUB" rates) (aget "EUR/RUB" rates) (aget "GBP/RUB" rates) brent btc)))
  95. ;;; Hooks
  96. (def-message-cmd-handler handler-rates (:rates)
  97. (multiple-value-bind (ts usd eur gbp brent btc) (db/get-last-finance)
  98. (bot-send-message chat-id
  99. (format nil "Зеленый *~,2F*, гейро *~,2F*, британец *~,2F*, чёрная *~,2F*, btc *~,2F* @ _~A_"
  100. usd eur gbp brent btc
  101. (format-ts (local-time:unix-to-timestamp ts)))
  102. :parse-mode "Markdown")))
  103. (def-message-cmd-handler handler-charts (:charts)
  104. (telegram-send-chat-action chat-id "upload_photo")
  105. (let* ((args (mapcar 'string-downcase args))
  106. (all-fields (mapcar #'car +finance-db-map+))
  107. (fields (or (intersection args all-fields :test 'equal) all-fields))
  108. (day-range (some #'(lambda (a) (aget a +chart-ranges+)) args))
  109. (number (some #'(lambda (a) (parse-integer a :junk-allowed t)) args))
  110. (avg (* 60 (cond
  111. (day-range (round day-range *chart-points*))
  112. (number)
  113. (:otherwise 1))))
  114. (after-ts (local-time:timestamp- (local-time:now)
  115. (* avg *chart-points*) :sec))
  116. (rates (multiple-value-list (db/get-last-finance)))
  117. (chart (apply #'make-chart (multiple-value-list
  118. (db/get-series after-ts fields avg)))))
  119. (telegram-send-photo chat-id chart
  120. :caption
  121. (format nil "Зеленый ~,2F, гейро ~,2F, британец ~,2F, чёрная ~,2F, btc ~,2F @ ~A"
  122. (elt rates 1) (elt rates 2) (elt rates 3) (elt rates 4) (elt rates 5)
  123. (format-ts (local-time:unix-to-timestamp (elt rates 0)))))))