1
0

finance.lisp 6.6 KB

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